In javascript we have something called this
.
this
is a little complex to explain and understand.
Did you know that you can use this
also in a Typescript interface
?
Here is how this this
work π
this
in Typescript interface
this
in a Typescript interface
allows to reference the structure of the interface
itself:
interface MyInterface {
value: string;
myInterface: this[]; // `MyInterface[]` β¨
}
The type of myInterface
is an array of MyInterface
.
We can built an interface
that references itself recursively:
const myInterface: MyInterface = {
value: "1",
myInterface: [
{
value: "2",
myInterface: [],
},
],
};
Using this
you can build a binary Tree
interface for example:
interface Tree<T> {
value: T;
leftBranch: Tree<T>[];
rightBranch: Tree<T>[];
}
const tree: Tree<string> = {
value: "a",
leftBranch: [{ value: "a-0", leftBranch: [], rightBranch: [] }],
rightBranch: [
{
value: "b",
leftBranch: [],
rightBranch: [
{
value: "c",
leftBranch: [{ value: "c-1", leftBranch: [], rightBranch: [] }],
rightBranch: [{ value: "c-2", leftBranch: [], rightBranch: [] }],
},
],
},
],
};
Recursive interface
in Typescript
Since this
references the structure of the interface
we can built any recursive type. We can use any of the Typescript's keywords to extract information from the interface
.
For example, you can use keyof
to extract the keys of an object inside the same interface
:
interface KeysInterface {
value: { a: number; b: unknown; c: string };
keys: keyof this["value"];
}
const keysInterface: KeysInterface = {
value: { a: 1, b: "", c: "" },
keys: "b", // "a" | "b" | "c"
};
this
using extends
This is what makes this
interesting:
The actual type of
this
is not static but it is based on the latest type in anextends
chain
Look at this type definition for example:
interface Keys {
value: { a: number };
}
interface Extended extends Keys {
value: this["value"] & { b: number };
}
Noticed the issue here?
The
Extended
type is invalid with the following error:Type instantiation is excessively deep and possibly infinite. ts(2589)
.
Why is that?
My initial intuition was as follows:
Keys
has avalue
- Since
Extended
extendsKeys
, alsoExtended
hasvalue
this["value"]
is extracting the type ofvalue
fromKeys
({ a: number }
)
That's the trick! this
does not reference the original value
from Keys
.
this
instead references the value
of Extended
itself.
By using this["value"]
we are therefore creating an unresolvable reference. The type of value
depends on itself, hence the infinite type error ππΌββοΈ
There is more π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
this
and unknown
Consider the following interface
:
interface Model {
x: unknown;
y: this["x"];
}
y
references the type of x
from Model
. Therefore in this example y
is of type unknown
:
type YModel1 = Model["y"]; // `unknown`
Now, can you guess what is the type of y
in the code below instead? π
type YModel2 = (Model & { x: number })["y"];
Let's analyze step by step:
type M = Model & { x: number }
type M = { x: unknown; y: this["x"] } & { x: number }
type M = { x: unknown & number; y: this["x"] }
type M = { x: number; y: this["x"] }
type M = { x: number; y: number } // πͺ
Here we apply again the this
magic trick! this["x"]
is not resolved to the original type of x
(unknown
).
Instead, Typescript first applies the intersection (&
) of unknown & number
on x
.
Every type intersected with unknown
resolves to itself:
type T1 = unknown & null; // `null`
type T2 = unknown & number; // `number`
type T3 = unknown & never; // `never`
type T4 = unknown & unknown; // `unknown`
type T5<T> = T & unknown; // `T`
type T6 = unknown & any; // `any`
Therefore x
"becomes" of type number
. Only then y
will be resolved, magically becoming also of type number
:
type YModel2 = (Model & { x: number })["y"]; // `number`
Higher-Kinded Types in Typescript
Turns out that this simple "trick" is at the core of encoding Higher-Kinded Types in Typescript.
This encoding is used in libraries like Effect to bring Higher-Kinded Types in Typescript
Explaining Higher-Kinded Types in Typescript requires a full post by itself.
Here below you can see the encoding (we are going to learn more about it in a follow up post):
export interface TypeLambda {
readonly Target: unknown;
}
export interface ArrayTypeLambda extends TypeLambda {
readonly type: Array<this["Target"]>;
}
const arrayTypeLambda: ArrayTypeLambda = {
Target: "unknown", // `unknown`
type: [], // `unknown[]`
};
export type Kind<F extends TypeLambda, Target> = F extends {
readonly type: unknown;
}
? (F & { readonly Target: Target })["type"]
: {
readonly F: F;
readonly Target: (_: Target) => Target;
};
type ArrayKind = Kind<ArrayTypeLambda, string>; // `string[]`
That's it!
You can experiment with the full Typescript code from the following Playground Link.
this
is always been tricky to get in javascript. Well, turns out that the same could be said for Typescript ππΌββοΈ
The fact that this
can encode Higher-Kinded Types in Typescript in so few lines of code is great! It unlocks many interesting implementations (see Effect π)
If you are interested in learning more about Typescript you can subscribe to the newsletter here below π
Thanks for reading.