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
  }
}

Playground

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

Playground

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;

Playground

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;

Playground

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;

Playground

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'

Playground

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

Playground

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

Playground

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);
  }
}

Playground

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: [],
};

Playground


More coming soon !


Suggestions