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!