all articles

TypeScript Advanced Type

2019-06-27 @sunderls

typescript





ref https://www.typescriptlang.org/docs/handbook/advanced-types.html

Union |

这个比较简单了就是“或者”的意思

type a = string | number | undefined

Intersection &

这个就是“且”的意思了。

比如这样一个merge object的方法

const merge = (a:object, b:object) => {
    return {
        ...a,
        ...b
    }
}

const c = merge({a: 3}, {b: 4}) // ts不知道c的type,只知道c是个object

为了让ts知道更多,我们需要告诉它返回值是两个参数的“且”,需要要用到generic

const merge = <T, S>(a: T, b: S): T & S => {
    return {
        ...a,
        ...b
    }
}
const c = merge({a: 3}, {b: 4}) 

如此TS就知道c的type是

{
    a: number;
} & {
    b: number;
}

嗯, nice

Type Guards

比如以下一种union type,它有可能是其中之一

type A = {
    a: number
}

type B = {
    b: number
}
type SomeThing = A | B

const getData  = (): SomeThing => {
    return Math.random() > 0.5 ? {a: 3} : {b: 4}
}

const c = getData()

此时TS不知道c是A还是B。如果要知道的话,可以简单这么写

if ((a as A).a) {
    // a 是 A
} else {
    // 这里因为是else里面了,所以不是A,那就是B。但是ts并不能进行推断
    // 因为 as 只是一种类型转换,并不是推理
}

为了解决这个问题,可以定一个guard,一种特殊的type的function来告诉ts是那种

// 这个方法的返回值是 `x is A`,嗯就是某种推理
const isA = (x: any): x is A {
    return typeof x.a !== 'number'
}

if (isA(a)) {
    // a 是 A
} else {
    // a 是B
}

如果A B是基本类型比如string number啥的,ts是知道的,不用这么写,可以直接

if (typeof A === 'string') {
  // string
} else {
  // 不是string,所以就是number
}

null check

strictNullChecks未开启的情况下, null可以传给任何type,也就是说let a:string = null 是合法的

用generic 定义tree

比如定一个二叉树节点,TreeNode

type TreeNode<T> = {
   value: number
   lef?t: TreeNode<T>
   right?: TreeNode<T>
}

const numTree = TreeNode<number>

Discriminated Unions/ tagged unions / algebraic data types

一种ts可以简单理解的方式来区分union type,需要在union的type中都有一个key prop来区分 比如


interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
    switch (s.kind) { // kind被用来当作了guard,ts可以用来断定type
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

可以看到和自己写的type guard相比,自动的guard要求比较严格,需要所有的type都有同样的一个key, 而且type也需要一样,必然就不行

this

this是一个type,可以用来chain method

class SomeClass {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
}

never

一个function如果走到了最后啥也没返回,那返回值是void。 一个function如果走不到最后,那就是返回never

const c = ()=> {
    throw new Error('s')
}
// never

const c = ()=> {
}
// void

keyof

就是object的key的union

let taxi: Car = {
    manufacturer: 'Toyota',
    model: 'Camry',
    year: 2014
};

type Key = keyof typeof taxi

Key 就是 'manufacturer'|'model' | 'year'

这样我们可以来定义lodash的pick方法了

_.pick(object, ['a', 'c']);

首先第二个参数需要是第一个参数的key,所以用generic constraint (extends)来限制第二个参数的type

const pick = <T, K >(obj: T, keys: K) // 本来是这样

const pick = <T, K extends keyof T>(obj: T, keys: K[]): T[K][] => {
 return keys.map(key => obj[key])
}

const picked = pick({a: 3, b: 'string'}, ['a'])

上述picker会被理解为 number[] 非常好

index signiture

interface Dictionary<T> {
    [key: string]: T;
}

也就是说Dictionary的key不固定,是泛的string,这种时候keyof Dictionary 就会是 string ,因为ts也不知道具体是啥

Mapped types

把一个type 映射到一个新的type,有点像一个map()处理

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

type M = {
    a: string
}

type MRead = ReadOnly<M>

let a:MRead = {
    a: 'string'
}

a.a = '4' // 报错,因为readonly

ReadOnly 已经是built in了可以直接用。

注意到语法是 [P in keyof T] ,所以in是一种循环map的感觉

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

// Flags和下面这种写法是一样的
type Flags = {
    option1: boolean;
    option2: boolean;
}

Partial

有了 Mapped Types,可以定义Partial了

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

其实就是给每一个key加上?允许其是undefined.

Partial也是builtin

Record

同样的可以定义如下

type Record<K extends string | number | symbol, T> = { [P in K]: T; }

type A = Record<string, string> 

// 实际上和下面写法一致,因为 string代表了所有的string,只能用index signature来理解

type A = {
    [x: string]: string
}

// 同理 Record<'a'| 'b', string>就是下面这种理解

type B = {
  a: string,
 b: string
}

Conditional Types

给type加上if条件。extends就有点像if

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"

Distributive conditional types

Conditional Type如果收到一个union type参数会自动展开。 也就是说

type Diff<T, U> = T extends U ? never : T; // 如果T包含了U,就不管,否则是type T

// 然后把这个contional应用于一个union
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // T30 就等于了 "b" | "d"

这个就是builtin的Exclude <T, U> – 差集

同样的可以做交集了

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

这就是builtin的Extract <T, U> – 交集

type NonNullable<T> = Diff<T, null | undefined>;

NonNullable 也是builtin,去掉null和undefined

Infer

Infer在extends条件句之下,可以从一个变量来推断类型,当然是动态的

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

type Func = () => string

type A = ReturnType<Func> // A 是string

这里就有点晕了。

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

这个比较好理解。然后infer R用来修饰第一个R,用来可以用来返回,相当于一个占位符的感觉,比如正则表达中匹配的内容可以用括号扩起来,然后$1, $2来表示。

ReturnType也是built in

ReturnType检查的是function,如果是特殊的构造函数(new)的话,就可以写出另外一个builtin type InstanceType

type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any

class C {
    x = 0;
    y = 0;
}

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // never
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error


如果觉得有帮助到你的话,
欢迎支付宝donate