This post will get updated from time to time.
Last Update: 11/1/2022 16:53 CET
TL;DR: GitHub-Gist Cheatsheet
Skip all the explanations and just see types
Also on dev.to
I've grown pretty fond of Typescript in recent years. In particular I very much like its Turing-complete type system.
Typescript provides a fair share of built-in Utility-Types, however, I always come back copy-pasta-ing from my old code when needing more complex util-types.
So here's a kind of cheat-sheet, with examples and explanations, for some of the advanced types I've built and used within the last years.
Also, check out SimplyTyped or TypeFest. They contain some truly mind-boggling, but extremely useful stuff. I try not to include it in every one of my projects, so there might be some duplicates.
As I find it pretty hard to explain more in-depth types, let me know where you would like to see clarification.
Also let me know if you know any other cool, usefull or mindbending stuff!
Without further ado, Let's dive in
Await Type
This one is pretty straightforward.
We use the built-in PromiseLike type to check if the input really is a promise.
If it is, use the infer
keyword to deduce its type-argument and recursively 'call' Await again.
If not, just return the input.
type Await<T> = T extends PromiseLike<infer U> ? Await<U> : T;
const asyncFn = async (): Promise<number> => new Promise((res) => res(1337));
type ResolvedReturnType = Await<ReturnType<typeof asyncFn>>; // = number
Bonus: You can also alter this slightly to check if some type is a Promise
export type IsPromise<T> = T extends PromiseLike<infer U> ? true : false;
const asyncFn = async (): Promise<number> => new Promise((res) => res(1337));
const syncFn = (): number => 1337;
type IsPromiseA = isPromise<ReturnType<typeof asyncFn>>; // = true
type IsPromiseB = isPromise<ReturnType<typeof syncFn>>; // = false
Length Type
Another simple one is the Length type.
You can use this for example to infer the number of parameters to a given function.
There is, however, another clever use, to which we will come back later
export type Length<T> = T extends { length: infer L } ? L : never;
type MyFnType = (a: number, b: string, c: number) => unknown;
type NumParams = Length<Parameters<MyFnType>>; // = 3
KeysOfType
I find this one particularly useful from time to time.
It returns the keys of a given interface O
, but only if they are of type T
export type KeysOfType<O, T> = {
[K in keyof O]: O[K] extends T ? K : never;
}[keyof O];
// example
interface MyInterface {
keyA: number,
keyB: string,
keyC: Record<string, MyInterface>,
keyD: 1337
};
type KeysOfTypeNumber = KeysOfType<MyInterface, number>; // 'keyA' | 'keyD'
This one is not as easy to unserstand, so how does it work?
First, we create a new type that contains all of the keys of O
,
then we get the valueType of O
by its key O[K]
and then check if it is of type T
If so, we just use the key K
as our value, otherwise, we use never
.
This would result in something like { keyA: 'keyA'; keyB: never }
The last step now is to look up this intermediate type, by all the keys of our interface O
.
The never
type gets discarded when looking up a type like this.
But because of this, if we wanted to filter our interface, we cannot just leave out the lookup.
Bonus: PickByType
// ConvertLiterals would convert literal types like `1337` to their base type like `number` if set to true
export type PickByType<O, T, ConvertLiterals extends boolean = false> = {
[K in KeysOfType<O, T>]: ConvertLiterals extends true ? T : O[K]
};
type PickedByNumber = PickByType<MyInterface, number>; // { keyA: number; keyD: 1337 }
OneOf
Suppose you have an Interface (or intersecting Interfaces) and you want to create a (strict) Union type from it.
In the aforementioned SimplyTyped library, there exist types with which you can do the same thing.
I personally like this version better, though, as it's a single 'one-liner' (cough) type that one can even parameterize.
Let's start with just the type
export type OneOf<T, Strict extends boolean = true> = {
[OuterKey in keyof T]: Strict extends false
? { [K in OuterKey]: T[K] }
: { [InnerKey in OuterKey|keyof T]?: InnerKey extends OuterKey ? T[OuterKey] : never } & { [TheKey in OuterKey]: T[OuterKey] }
}[keyof T];
Now break this down somewhat.
First, we omit the second type parameter Strict
and only use the then false-case and just look at a simpler type
export type Unionize<T> = {
[OuterKey in keyof T]: { [K in OuterKey]: T[K] }
}[keyof T];
This creates a type with the keys of the input type T
as key and a new dictionary type containing a single key-of T
and the corresponding value type of T
as value.
Afterward, we use the look-up trick we already used above to get a union type of the inner dictionary types.
interface MyIface {
keyA: number;
keyB: string;
}
type Demo = Unionize<MyIface>; // = { keyA: number } | { keyB: string }
However...
const a: Unionize<MyIface> = { keyA: 1337 }; // ok
const b: Unionize<MyIface> = { keyA: 1337, keyB: 'foo' }; // also okay
This is because union dictionary types are not exclusive.
In some cases, it can be okay, other times you'd want it to be strict.
So let's look at that
export type OneOf<T> = {
[OuterKey in keyof T]: {
[InnerKey in OuterKey|keyof T]?: InnerKey extends OuterKey ? T[OuterKey] : never
} & {
[TheKey in OuterKey]: T[TheKey]
}
}[keyof T];
Now, this is a bit more complex.
If we look closely, however, it is the same as the Unionize
type shown above except for an intersecting type: { [InnerKey in OuterKey|keyof T]?: InnerKey extends OuterKey ? T[OuterKey] : never }
.
Let's break this down some more.
OuterKey
is a single key of our input-type T
.
Create a union with that and with all keys of T
as keys for the inner dictionary ([InnerKey in OuterKey|keyof T]: ...
).
For the value of the inner dictionary, we check if the current InnerKey
is the OuterKey
( InnerKey extends OuterKey ...
).
If it is, we use the value-type of our original type T
, otherwise never
.
The last part is to make every key for this dictionary optional (the ?
after the key declaration).
Now this gives us a type which, has all keys of the input-type, but only a single key is allowed to be assigned with a value-type other than undefined.
That means we can still use undefined
, though.
To mitigate this, we add an intersection to an inner dictionary, that being the one we've seen in the Unionize
type, as it doesn't allow undefined.
The result is something like this
export type OneOf<T, Strict extends boolean = true> = {
[OuterKey in keyof T]: Strict extends false
? { [K in OuterKey]: T[K] }
: {
[InnerKey in OuterKey|keyof T]?: InnerKey extends OuterKey ? T[OuterKey] : never
} & {
[TheKey in OuterKey]: T[TheKey]
}
}[keyof T];
// example
interface MyInterface {
keyA: number,
keyB: string,
keyC: Record<string, unknown>,
keyD: 1337
};
type OnlyOne = OneOf<MyInterface>; // ^= more or less strict version of { keyA: number } | { keyB: string } | { keyC: Record<string, MyInterface> } | { keyD: 1337 }
type NonStrictVersion = OneOf<MyInterface, false>; // { keyA: number } | { keyB: string } | { keyC: Record<string, MyInterface> } | { keyD: 1337 }
Tuple Manipulation
Tuples, combined with the ability to spread them and infer single or even rest-types, are an incredibly useful tool when dealing with more complex types.
So here are a few basic manipulation types starting with:
Push, PushFront, Pop, PopFront, Shift, ShiftRight
type Push<T extends unknown[], U> = T extends [...infer R] ? [...T, U] : never;
type PushFront<T extends unknown[], U> = T extends [...infer R] ? [U, ...T] : never;
type Pop<T extends unknown[]> = T extends [...infer R, infer U] ? U : never;
type PopFront<T extends unknown[]> = T extends [infer U, ...infer R] ? U : never;
type Shift<T extends unknown[]> = T extends [infer U, ...infer R] ? R : never;
type ShiftRight<T extends unknown[]> = T extends [...infer R, infer U] ? R : never;
These should all be pretty straightforward.
Note: You could just as well use any
instead of infer X
where X
isn't used.
Reverse
Since there are no loop-functions for TypeScripts type-system, and we somehow need to iterate over the input tuple, we have to use recursion.
To check when the recursion needs to end, we can use the aforementioned Length
-type. Afterward, just Pop from one tuple, and Push to another.
type Reverse<T extends unknown[], U extends unknown[] = []> = Length<T> extends 1 ? Push<U, Pop<T>> : Reverse<ShiftRight<T>, Push<U, Pop<T>>>;
A little bit special here is the second type-parameter, which is default initialized to an empty tuple.
We basically use that as our return value, after recursively filling it from the input T
.
If you wanted to restrict any other devs in your codebase to do nasty things with the second type parameter, you can just wrap the above implementation.
Filter, TupleIncludes
Next, let's look at a filter-type. This one's also pretty simple.
First, check if our tuple is empty. If it's not, infer the first Element F
and the rest of the tuple as another type R
.
Then we check if F
is equal to, or extends, the type we want to filter out.
If it is, return Filter again, but this time on the rest R
without F
.
Otherwise, we return a new tuple, with F
at the front and a spread (...) of the filtered-rest.
Combining the Filter type with our handy-dandy Length type, we can also build an Includes-type by simply comparing the lengths of the filtered and unfiltered tuple!
type Filter<T extends unknown[], U> = T extends [] ? [] : T extends [infer F, ...infer R] ? F extends U ? Filter<R, U> : [F, ...Filter<R, U>] : never
type TupleIncludes<T extends unknown[], U> = Length<Filter<T, U>> extends Length<T> ? false : true
You may have noticed that I didn't actually call the Includes-type like that, but rather TupleIncludes.
We'll come back to that just below...
Playground
Template Literal Types
introduced in TypeScript 4.1, they really filled a gap for me.
With them, you can do so much fancy and mindblowing stuff!
Someone even wrote a full-blown CSS-Parser 🤯!
Fancyness aside, I also want to share a few types I've found useful to me.
StringIncludes
The nice part about template literals is that you can infer literal types from them.
Even nicer: You can infer empty strings!
Which makes a type that can check if any string includes another pretty trivial.
type StringIncludes<S extends string, D extends string> = S extends `${infer T}${D}${infer U}` ? true : false;
type Demo1 = StringIncludes<'a.b', '.'>; // = true
type Demo2 = StringIncludes<'ab', '.'>; // = false
type Demo3 = StringIncludes<'a.', '.'>; // = true
type Demo4 = StringIncludes<'.', '.'>; // = true
As we now also have a StringIncludes type, we can now combine that with the TupleIncludes from above, to get a completely generic Includes type
type Includes<T extends unknown[]|string, U> = T extends unknown[] ? TupleIncludes<T, U> : T extends string ? U extends string ? StringIncludes<T, U> : never : never;
Template literal types are, however, for some reason, more restricted than tuples, so we need to build a few types to help with that.
Split
type Split<S extends string, D extends string> =
string extends S ? string[] :
S extends '' ? [] :
S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];
You may wonder why the first conditional is `string extends S`.
If you haven't guessed, it's the check if `string` itself is passed as a type argument, instead of any literal.
Any literal string extends string - but string doesn't extend any literal string
Here's some code to better explain:
type Demo<S extends string> = string extends S ? 'used string' : `used literal: ${S}`;
type D1 = Demo<'str'>; // = 'used literal: str'
type D2 = Demo<string>; // = 'used string'
Join
type Join<T extends unknown[], D extends string> = string[] extends T ? string : T extends string[]
? PopFront<T> extends string ? Length<T> extends 1 ? `${PopFront<T>}` : `${PopFront<T>}${D}${Join<Shift<T>, D>}` : never
: never;
Using what we've learned so far and some of the tuple manipulation types above, we can now join any string tuple back to a string.
Example using all of this
A cool and really useful example, using most of the above util-types, is dealing with "Paths" and nested-types of dictionaries.
(Explanations for those are left as an excersice for the reader)
Let's start with valid Paths
ValidPaths, ValidPathTuples
At the time of initially writing this, the Tuple version was the only one working.
In most recent TS versions both work, however, the string literal version has got some gotchas and so the tuple version is somewhat preferred.
const dictionary = {
someProp: 123,
nested: {
moreProps: 333,
deeper: {
evenDeeper: {
deepest: 'string'
}
},
alsoDeeper: {
randomProp: {
anotherProp: 'wtf'
}
}
}
} as const;
type MyDict = typeof dictionary;
// If you look at type "Debug" below, this surprisingly works.
// You may get a `Type instantiation is excessively deep and possibly infinite.`-error if you're using an older TS version, though
type ValidPaths<T> = keyof T extends never ? never : ({
[K in keyof T]: T[K] extends never ? never : T[K] extends Record<string|number|symbol, unknown>
? K extends string ? `${K}.${ValidPaths<T[K]>}` | K : never
: K
})[keyof T] & string;
type Debug = ValidPaths<MyDict>;
// With Tuples.
type ValidPathTuples<T> = keyof T extends never ? never : ({
[K in keyof T]: T[K] extends never ? never : T[K] extends Record<string|number|symbol, unknown>
? [K, ...ValidPathTuples<T[K]>] | [K]
: [K]
})[keyof T];
type DebugTuples = ValidPathTuples<MyDict>;
// on recent TS versions you can even join the tuples back to "PathStrings"
type DebugTuples1 = Join<ValidPathTuples<MyDict>, '.'>;
NestedType, NestedTypeByTuple
We can also craft a type that gets us the nested-type of a dictionary for any valid path, or throw an error if an invalid path is passed.
And of course we can put it all together in a beautiful function where only valid Paths are allowed as parameter and the return type is automagically deduced for us.
// string version
type NestedType<T, P extends string> = (
Includes<P, '.'> extends true
? PopFront<Split<P, '.'>> extends keyof T
? NestedType<T[PopFront<Split<P, '.'>>], Join<Shift<Split<P, '.'>>, '.'>>
: never
: P extends keyof T ? T[P] : never
);
type DemoNested1 = NestedType<MyDict, "nested.moreProps">; // 333
type DemoNested2 = NestedType<MyDict, "nested.deeper.evenDeeper">; // { readonly deepest: "string" }
type DemoNestedNever = NestedType<MyDict, "nested.randomProp">; // never
// tuple version
type NestedTypeByTuple<T, P extends string[]> = (
Length<P> extends 1
? Pop<P> extends keyof T ? T[Pop<P>] : never
: PopFront<P> extends keyof T ? Shift<P> extends string[]
? NestedTypeByTuple<T[PopFront<P>], Shift<P>>
: never : never
);
type DemoNestedTuple1 = NestedTypeByTuple<MyDict, ["nested" ,"moreProps"]>; // 333
type DemoNestedTuple2 = NestedTypeByTuple<MyDict, ["nested" , "deeper", "evenDeeper"]>; // { readonly deepest: "string" }
type DemoNestedTupleNever = NestedTypeByTuple<MyDict, ["nested", "sillyProp"]>; // never
// String version internally using tuples
// Bonus: Also errors now
type NestedTypeUsingTuplesAgain<T, P extends ValidPaths<T>> = NestedTypeByTuple<T, Split<P, '.'>>;
type Convoluted = NestedTypeUsingTuplesAgain<MyDict, 'nested.alsoDeeper'>;
type ConvolutedError = NestedTypeUsingTuplesAgain<MyDict, 'nested.nonExistant'>;
// And now we can finally give lodash's `get` function a run for it's money
function GetByPath<T, P extends ValidPaths<T>>(dict: T, path: P): NestedType<T, P> {
// internal impl. must be different. only dominhg types here!
return (dict as any)[path];
}