Typescript typing tricks
Nice Typscript typing you might not know ! (And will make you 💙 TS)
Matthieu Riegler -
Nice TS tricks I learned you might like
Assert unreachable path a compiler time
function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}
function foo(val: 1 | 2 | 3) {
switch (val) {
case 1:
case 2:
case 3:
return;
default:
assertUnreachable(val); // compilation error if a case isn't covered before
}
}
An Array type with at least N elements
type ArrayOfTwoOrMore<T> = [T, T, ...T[]];
type TwoOrMoreStrings = ArrayOfTwoOrMore<string>;
declare const myStrings: TwoOrMoreStrings;
const foo = myStrings[0]; // string
const bar = myStrings[1]; // string
const bar = myStrings[2]; // string | undefined
This one is useful particularly with noUncheckedIndexedAccess
enabled !
Removing/Removing types from an array type
Removing first item
type RemoveFirst<T extends Array<unknown>> = T extends [unknown, ...infer rest] ? rest : never;
Removing n first items
type RemoveFirstNItems<T extends unknown[], N extends number, Removed extends unknown[] = []> = Removed["length"] extends N ? T : T extends [infer First, ...infer Rest] ? RemoveFirstNItems<Rest, N, [...Removed, First]> : never;
Removing undefined from Array
type FilterUndefined<T extends unknown[]> = T extends [] ? [] : T extends [infer H, ...infer R] ? (H extends undefined ? FilterUndefined<R> : [H, ...FilterUndefined<R>]) : T;
Unions
Union of nested key
type AllUnionMemberKeys<T> = T extends any ? keyof T : never;
type AB = { a: string } | { b: string };
type ABnever = keyof AB; // never
type ABKey = AllUnionMemberKeys<AB>; // 'a' | 'b'
About unions
A union of functions it is only safe to invoke it with an intesection of parameters which in this case resolves to never
type Foo = ((foo: number) => void) | ((foo: string) => void);
declare const foo: Foo;
foo(); // can't call it
In short, using a union of function is usually a bad idea.
Literal templates
Ensure a binary string
type BinDigit = "0" | "1";
type OnlyBinDigit<S> = S extends "" ? unknown : S extends `${BinDigit}${infer Tail}` ? OnlyBinDigit<Tail> : never;
type BinDigits = string & { __brand: "onlydigits" };
declare function onlyBinDigit<S extends string>(s: S & OnlyBinDigit<S>): BinDigits;
const a = onlyBinDigit("01010101010011"); // OK
const notBin = onlyBinDigit("010101012"); // NOK
In this case here, using a banded type will ensure that the string is not only a string but a string with only 0/1.
Basic Generic Factory
type ConstructorArguments<T> = T extends new (...args: infer P) => any ? P : never;
class Foo {
constructor(private foo: string, private bar: number) {}
}
export class Factory {
public create<T extends new (...args: Array<any>) => any>(constr: T, ...params: ConstructorArguments<T>): InstanceType<T> {
return new constr(...params);
}
public foo() {
this.create(Foo, "foo", 1);
}
}
Unit test the type system & expecting error
Sometimes we want to ensure that some types/parameters are considered invalid.
declare function foo(bar: string): void;
foo("3"); // OK
// @ts-expect-error
foo(3); // Also OK
But if we change the type of foo
we'll get following :
declare function foo(bar: string | number): void;
// @ts-expect-error <== Unused '@ts-expect-error' directive.
foo(3); // KO
foo("3"); // OK
This means, this way we can have tests on the typings only & not relying on the runtime.
Extending a mapped type
Suppose you need an object with a generic key but also other fixed properties. Unfortunately Mapped types may not declare properties or methods. So the way to go is an intersection !
type Pagination<Key extends string, Content> = {
pagination: {
total: number;
page: number;
};
} & { [K in Key]: Content[] };
type Product = {};
type ProductData = Pagination<"products", Product>;
const productResponse: ProductData = {
pagination: { total: 100, page: 0 },
products: [],
};
More coming soon !