Edward Lee

โ† Back

Advanced Typescript

Conditional types

type NonNullable<T> = T extends null | undefined ? never : T;

Exclude

type MyExclude<T, U> = T extends U ? never : T;

// Example 1
type AB = Exclude<'A' | 'B' | 'C', 'C'>; // 'A' | 'B'

// Example 2
type SomeNumbers = MyExclude<'A' | 'B' | 1 | 2, string>; // 1 | 2

// Example 3
interface Person {
    id: number;
    name: string;
    age: number;    
}

function safeSetProp<T, K extends MyExclude<keyof T, 'id'>>(obj: T, key: K, value: T[K]) {
    obj[key] = value;
}

declare const obj: Person;
safeSetProp(obj, 'name', 'Miล‚osz');
safeSetProp(obj, 'id', 100); // ๐Ÿ”ด Error!

Extract

type MyExtract<T, U> = T extends U ? T : never;

// Example 1
type AB = MyExtract<'A' | 'B' | 'C', 'C'>; // 'C'

// Example 2
type SomeNumbers = MyExtract<'A' | 'B' | 1 | 2, string>; // 'A' | 'B'

// Example 3
interface StrangeObj {
    foo: string;
    bar: number;
    1: string;
    42: number;
}

function setStringProp<T, K extends MyExtract<keyof T, string>>(obj: T, key: K, value: T[K]) {
    obj[key] = value;
}

declare const obj: StrangeObj
setStringProp(obj, 'bar', 1);
setStringProp(obj, 42, 1); // ๐Ÿ”ด Error!

Omit

interface Person {
    id: number;
    name: string;
    age: number;    
}

type AnonymousPerson = Omit<Person, 'id'>;

const anonymousPerson: AnonymousPerson = { id: 1, name: 'John', age: 33 }; // ๐Ÿ”ด Error!

Parameter

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

const sayHello = (name: string, age: number) => `Hello ${name}, your age is ${age}`;
type SayHelloParams = Parameters<typeof sayHello>; // [string, number]

Return

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

const sayHello = (name: string, age: number) => `Hello ${name}, your age is ${age}`;
type SayHelloReturnType = ReturnType<typeof sayHello>; // string

Mapped types

Readonly

interface Person {
    name: string;
    age: number;
}

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

type ReadonlyPerson = Readonly<Person>;

DeepReadonly

type DeepReadonly<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

type Employment = DeepReadonly<{
    person: Person;
    company: string;
}>;

interface Person { name: string; }

declare const employment: Employment;
// employment.person.name = 'Milosz'; // ๐Ÿ”ด Error!

Partial

interface Settings {
  width: number;
  autoHeight: boolean;
}

type Partial<T> = {
    [P in keyof T]?: T[P];
};

const defaultSettings: Settings = {};

function getSettings(custom: Partial<Settings>): Partial<Settings> {
  return { ...defaultSettings, ...custom };
}

Required

type Required<T> = {
    [P in keyof T]-?: T[P];
};

type Foo = Required<{ name?: string }>; // { name: string; }

Pick

interface Person {
  name: string;
  age: number;
  id: number;
}

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type NameAndAge = Pick<Person, 'name' | 'age'>; // { name: string; age: number; }

Record

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

type StringDictionary = Record<string, string>; // { [x: string]: string; }
type ABCNumbers = Record<'a' | 'b' | 'c', number>; // { a: number; b: number; c: number; }

ReturnTypes

type ReturnTypes<T> = {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? ReturnType<T[K]> : never;
}

type Test = ReturnTypes<{
    a: string;
    b: (a: number) => number;
}>; // { a: never; b: number; }

Type guards

// typeof
if (typeof width === "number") {
      this.width = width;
}

// instanceof
if (obj instanceof Person) {
    console.log(obj.name);
}

// custom 
interface Article {
  title: string;
  content: string;
}

function isArticle(object: any): object is Article {
  return "title" in object && "content" in object;
}

Structural vs Nominal types

interface Cat {
    name: string;
    breed: string;
}

interface Dog {
    name: string;
    breed: string;
}

const cat: Cat = { name: 'Filemon', breed: 'Chartreux' };
let dog: Dog = cat; // โœ… No error

vs

type UserId = string & { __brand: "UserId" };
type ArticleId = string & { __brand: "ArticleId" };

declare const userId: UserId;

function getArticle(articleId: ArticleId) {}

getArticle(userId); // ๐Ÿ”ด Error!

nominal type

type Nominal<T, K extends string> = T & { __brand: K };

type UserId = Nominal<string, "UserId">;
type ArticleId = Nominal<string, "ArticleId">;

example

type Nominal<T, K extends string> = T & { __brand: K };

type NonEmptyString = Nominal<string, "nonempty">;

const isNonEmpty = (s: string): s is NonEmptyString => s.length > 0;

const foo = (s: NonEmptyString) => console.log(s);

const bar = "abcd";
foo(bar); // error!

if (isNonEmpty(bar)) {
  foo(bar); // OK
}