I’ve been using typescript for a long time, but I don’t feel like I’ve used it completely. Because a lot of typescript features are not used, view the code written before the screen full of any, so it is easy to cause a lot of bugs, but also did not play the typescript real “type” power. This article summarizes some tips for using typescript, which can be used when using typescript in the future.

Without further ado, let’s get straight to the code.

Function Overloading

When you want to pass the user parameter without the flag, and the para parameter with the flag, you can write it like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface User {
  name: string;
  age: number;
}

const user = {
  name: 'Jack',
  age: 123
};

class SomeClass {

  public test(para: User): number;
  public test(para: number, flag: boolean): number;

  public test(para: User | number, flag?: boolean): number {
    // Specific implementation
    return 1;
  }
}

const someClass = new SomeClass();

// ok
someClass.test(user);
someClass.test(123, false);

// Error
// someClass.test(123); 
//Argument of type 'number' is not assignable to parameter of type 'User'.
// someClass.test(user, false);
//Argument of type '{ name: string; age: number; }' is not assignable to parameter of type 'number'.

Mapping Type

Before understanding mapping types, you need to understand keyof, never, typeof, in.

keyof: keyof takes the key of the interface

1
2
3
4
5
6
7
interface Point {
    x: number;
    y: number;
}

// type keys = "x" | "y"
type keys = keyof Point;

never: the type of the value that never exists

Official Description.

the never type represents the type of values that never occur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Example: Performing a comprehensive compile-time check
type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // Here foo is narrowed to a string type
  } else if (typeof foo === "number") {
    // Here foo is narrowed to a number type
  } else {
    // foo in this case is never
    const check: never = foo;
  }
}

The purpose of using never to avoid the possibility of adding a union type without a corresponding implementation is to write type-absolutely safe code.

typeof: takes the type of a value

1
2
3
4
const a: number = 3

// Equivalent to: const b: number = 4
const b: typeof a = 4

in: checks if an attribute exists on an object

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface A {
  x: number;
}

interface B {
  y: string;
}

function doStuff(q: A | B) {
  if ('x' in q) {
    // q: A
  } else {
    // q: B
  }
}

Mapping types is the mapping of one type to another type, which simply means that the new type converts each property of the old type in the same form.

Partial, Readonly, Nullable, Required

  • Partial converts each attribute to an optional attribute
  • Readonly Converts each property to a read-only property
  • Nullable Converts each property to the union of old and null
  • Required Converts each property to a required property
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
type Partial<T> = {
    [P in keyof T]?: T[P];
}

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

type Nullable<T> = { 
  [P in keyof T]: T[P] | null 
}

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

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

type PersonPartial = Partial<Person>;
type PersonReadonly = Readonly<Person>;
type PersonNullable = Nullable<Person>;

type PersonPartial = {
    name?: string | undefined;
    age?: number | undefined;
}

type PersonReadonly = {
    readonly name: string;
    readonly age: number;
}

type PersonNullable = {
      name: string | null;
      age: number | null;
}

interface Props {
  a?: number;
  b?: string;
}

const obj: Props = { a: 5 };

const obj2: Required<Props> = { a: 5 };
// Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

Pick, Record

  • Pick picks a set of properties to specify a new type
  • Record Creates a set of properties to specify a new type, often used to declare ordinary Object objects
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}

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

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

todo; // = const todo: TodoPreview


interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";

const nav: Record<Page, PageInfo> = {
  about: { title: "title1" },
  contact: { title: "title2" },
  home: { title: "title3" },
};

nav.about; // = const nav: Record

Exclude, Omit

  • Exclude removes the intersection and returns the remainder
  • Omit Exclude for key-value pair objects, removes the key-value pairs contained in the type
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type Exclude<T, U> = T extends U ? never : T
type Omit = Pick<T, Exclude<keyof T, K>>

// 相当于: type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
  title: "a",
  completed: false,
};

ReturnType

Get the return value type, generally a function

1
2
3
4
5
6
7
8
9
type ReturnType<T extends (...args: any) => any>
  = T extends (...args: any) => infer R ? R : any;

declare function f1(): { a: number; b: string };
type T1 = ReturnType<typeof f1>;
//    type T1 = {
//        a: number;
//        b: string;
//    }

There are many more mapping types, see Utility Types reference.

Type Assertion

Type assertions are used to explicitly tell typescript the detailed type of a value, and can be used wisely to reduce our workload.

For example, a variable doesn’t have an initial value, but we know its type information (it may be returned from the backend) what’s the best way to derive the type information correctly and still have it work? One online recommendation is to set the initial value and then use typeof to get the type (which may be used elsewhere). It is also possible that using type assertions can solve this type of problem

1
2
3
4
5
6
7
8
interface User { 
    name: string; 
    age: number; 
} 

export default class someClass { 
    private user = {} as User;
} 

Enumeration

Enumerated types are divided into numeric types and string types, where numeric enumerations can be used as flags

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
enum AnimalFlags {
    None = 0, 
    HasClaws = 1 << 0, 
    CanFly = 1 << 1, 
    HasClawsOrCanFly = HasClaws | CanFly 
}

interface Animal { 
    flags: AnimalFlags; 
   [key: string]: any; 
} 

function printAnimalAbilities(animal: Animal) { 
    var animalFlags = animal.flags; 
    if (animalFlags & AnimalFlags.HasClaws) { 
        console.log('animal has claws'); 
    } 
    if (animalFlags & AnimalFlags.CanFly) { 
        console.log('animal can fly'); 
    } 
    if (animalFlags == AnimalFlags.None) { 
        console.log('nothing'); 
    } 
} 

var animal = { flags: AnimalFlags.None }; 
printAnimalAbilities(animal); // nothing 
animal.flags |= AnimalFlags.HasClaws; 
printAnimalAbilities(animal); // animal has claws 
animal.flags &= ~AnimalFlags.HasClaws; 
printAnimalAbilities(animal); // nothing 
animal.flags |= AnimalFlags.HasClaws | AnimalFlags.CanFly; 
printAnimalAbilities(animal); // animal has claws, animal can fly 
  • using |= to add a flag.
  • using a combination of &= and ~ to clear a flag.
  • | to merge flags.

This may not be commonly used, but we can see similar code in the typescript source code about types.

Enumerations of string types can maintain constants.

1
2
3
4
5
6
7
8
9
const enum TODO_STATUS {
  TODO = 'TODO',
  DONE = 'DONE',
  DOING = 'DOING'
}

function todos (status: TODO_STATUS): Todo[];

todos(TODO_STATUS.TODO)

Tuple

Represents an array with a known number and type of elements, the type of each element need not be the same.

1
2
let x: [string, number];
x = ['hello', 10]; 

When issuing a variable number of requests, you can apply.

1
2
3
4
5
const requestList: any[] = [http.get<A>('http://some.1')]; // Set to any[] type 
if (flag) { 
    requestList[1] = (http.get<B>('http://some.2')); 
} 
const [ { data: a }, response ] = await Promise.all(requestList) as [Response<A>, Response<B>?]

Generics

After defining a generic type, there are two ways to use it, one is to pass in the generic type, and the other uses type inference.

1
2
3
declare function fn<T>(arg: T): T; // Define a generic function 
const fn1 = fn<string>('hello'); // The first way, passing in a generic type 
string const fn2 = fn(1); // The second way is to infer that the generic T is of type number from the type number passed in as argument arg 

An example of a flat array structure to build a tree structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Data before conversion 
const arr = [ 
{ id: 1, parentId: 0, name: 'test1'}, 
{ id: 2, parentId: 1, name: 'test2'}, 
{ id: 3, parentId: 0, name: 'test3'} 
]; 
// After conversion 
[ { id: 1, parentId: 0, name: 'test1', 
    childrenList: [ { id: 2, parentId: 1, name: 'test2', childrenList: [] } ] }, 
    { id: 3, parentId: 0, name: 'test3', childrenList: [] } 
]


interface Item { 
    id: number; 
    parentId: number; 
    name: string; 
} 

// The options parameter is passed in to get the type of childrenKey, and then passed to the TreeItem

interface Options<T extends string> { 
    childrenKey: T; 
} 
type TreeItem<T extends string> = Item & { [key in T]: TreeItem<T>[] | [] }; 
declare function listToTree<T extends string = 'children'>(list: Item[], options: Options<T>): TreeItem<T>[]; 
listToTree(arr, { childrenKey: 'childrenList' }).forEach(i => i.childrenList) 

infer

Indicates the type variable to be inferred in the extends conditional statement.

1
type ParamType<T> = T extends (param: infer P) => any ? P : T; 

This statement means that if T can be assigned to (param: infer P) => any, the result is (param: infer P) => parameter P of type any, otherwise it returns T.

1
2
3
4
5
6
7
interface User { 
    name: string; 
    age: number; 
} 
type Func = (user: User) => void 
type Param = ParamType<Func>; // Param = User 
type AA = ParamType<string>; // string

Example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// [string, number] -> string | number
type ElementOf<T> = T extends Array<infer E> ? E : never;

type TTuple = [string, number];

type ToUnion = ElementOf<TTuple>; // string | number


// T1 | T2 -> T1 & T2
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type Result = UnionToIntersection<T1 | T2>; // T1 & T2

Summary

typescript is still very powerful about type restrictions, due to the limited article, there are other types such as union types, cross types, etc. Readers can check the information by themselves. I just started to touch the paradigm and its various combinations will feel unskilled, and will slowly apply it in the next project, trying to minimize the bugs.


Reference https://blog.dteam.top/posts/2021-08/ts-useful-tips.html