Manuel Rueda Blog

TypeScript Complex Types

April 03, 2019 - 3 minutes read

    Other languages:
  • Español 🇦🇷

TypeScript’s type system is Turing Complete and give us the power to create powerful but complex type definitions to power our codebase. This is feasible thanks to mapped types, recursive types, conditional types, index accessible types, union types and generic types.

This is cool but there is not much documentation or practical example about it, the TypeScript Advanced Types article has minimal examples and explanations about how the use of these.

In this article I will show some “real life” examples to show what can be achievable with this.

Self reference type

Let say that we want to create an object that has a property foo of type string and a property bar that its type is defined based on the value of foo.

interface FooTypesMap {
    one: string;
    two: boolean;
    three: {
        one: Date
    };
}

type MyType = {
    [k in keyof FooTypesMap]: {
        foo: k,
        bar: FooTypesMap[k],
    }
}[keyof FooTypesMap]


const instanceOne: MyType = {
    foo: 'one',
    bar: 'some value',
}

const instanceTwo: MyType = {
    foo: 'two',
    bar: true,
}

const instanceThree: MyType = {
    foo: 'three',
    bar: {
        one: new Date()
    },
}

In this example we are using mapped types, index accessible types and union types to achieve type that can mutate based on the values of itself.

Specific self reference type

If you play around with the previous example you will find out that MyType is an union type of all possible combinations and when you define foo value TypeScript can narrow those possibilities to only the ones that match foo === X. But what about if you want to declare a variable of a specific possibility of that type? That is not possible in the previous example but here is how you can achieve it.

interface FooTypesMap {
    never: never;
    one: string;
    two: boolean;
    three: {
        one: Date
    };
}

type MyType<TType extends keyof FooTypesMap = 'never'> = {
    [k in keyof FooTypesMap]: {
        foo: TType extends 'never' ? k : TType,
        bar: FooTypesMap[TType extends 'never' ? k : TType],
    }
}[keyof FooTypesMap]

let instanceFour: MyType<'three'>;

instanceFour = {
    foo: 'three',
    bar: {
        one: new Date()
    }
}

The difference with the previous example is that new added an optional generic type with a default value of never and use it inside a conditional type to verify if is declared or is the default. If is declared we use that type to define foo’s type and to get the type for bar and if its not defined we use k like the previous example.

The only hacky thing here is that never: never definition in FooTypesMap, that allow us to keep the type constraint TType extends keyof FooTypesMap that defines that TType has to be a key of FooTypesMap.

Recursive self reference type

There is one more level of type complexity that we can add to our MyType, what if we need an extra optional property called baz that is type MyType. This will allow us to use recursive types.

Also I will add a property called qux that is a map of N number of optional properties of type MyType to show how powerful can be.

Take in account that you probably want the recursive typed properties as optional because if not you will have a infinite declaration chain.

interface FooTypesMap {
    never: never;
    one: string;
    two: boolean;
    three: {
        one: Date
    };
}

type MyType<TType extends keyof FooTypesMap = 'never'> = {
    [k in keyof FooTypesMap]: {
        foo: TType extends 'never' ? k : TType,
        bar: FooTypesMap[TType extends 'never' ? k : TType],
        baz?: MyType,
        qux?: {
            [key: string]: MyType
        }
    }
}[keyof FooTypesMap]

let instance: MyType<'three'>;

instance = {
    foo: 'three',
    bar: {
        one: new Date()
    },
    baz: {
        foo: 'one',
        bar: 'some value'
    },
    qux: {
        myKey: {
            foo: 'two',
            bar: false,
        }
    }
}

Thanks Keshav for all the help with this typings!


Manuel Rueda

My name is Manuel Rueda and welcome to my blog. You can also follow me on Twitter and/or Github.