How "this" works in Typescript

Sandro Maglione

Sandro Maglione

Web development

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 an extends 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 a value
  • Since Extended extends Keys, also Extended has value
  • this["value"] is extracting the type of value from Keys ({ 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.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.

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.

👋・Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.