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
}