Blog.

Typescript typing tricks

Cover for Typescript typing tricks
.
Mis à jour le

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 !