Published on

自用 TS 手册

Authors

只会写基础TS就跟处处写重复代码不会抽通用模块、功能函数的 js 初学者一样一样的,run是能run,就是有点费键盘...

  • TS 官方文档, 基础部分建议通读,本文将记录一些有用且常用的 TS 概念和技巧

基础类型

权重:越往上越大,越往下越小。

ts-basic-types

基础运算:

  • |,联合类型
    • 存在父子关系,则结果是类型权重较的那个;没有父子关系则为联合类型
    • 在二进制位运算里,| 会有增加属性的功能,TS 里也类似,使得类型范围增加,但赋值时只能命中其中某一个类型
  • &,交叉类型
    • 存在父子关系,则结果是类型权重较的那个;没有父子关系则为 never
    • 在对象类型中就是合并对象类型

特殊的 any 既为顶部类型也为底部类型:

type t1 = any | unknown // any
type t2 = any & unknown // any

特殊的,never 在联合类型中会被过滤掉:

// 获取函数属性的键值
interface Demo {
    a: string
    b: number
    d: () => string[]
    e: () => string[]
    f: () => number[]
}
type GetDemoFnKeyTypes<T, K> = {
    [P in keyof T]: T[P] extends K ? P : never // TS 中的遍历属性,类似与 JS 的 forin 语句
}[keyof T]
type FnKeyTypes = GetDemoFnKeyTypes<Demo, () => string[]>  // 'd' | 'e'

取属性类型

JS 对象通过 .[prop] 获取属性值,TS 中则可以通过后者 [prop] 获取属性类型:

interface Person {
    name: string
    age: number
}
type Name = Person['name'] // string

/** 数组、元组、字符穿也可以通过索引获取对应位置的类型*/

特殊的,基础类型可以取到原型的定义:

// String.prototype.concat 的类型定义
type Concat = 'hello'['concat'] // (...strings: string[]) => string
// Number.prototype.toFixed
type ToFixed = 1['toFixed'] // (fractionDigits?: number | undefined) => string

keyof & typeof

keyof 获取公有属性的联合类型;typeof 推导出 js 变量的对应类型:

class Person {
  public name: string
  public age: number
  private gender: boolean
}
type T1 = keyof Person // 'name' | 'age'
type T2 = keyof any // string | number | symbol

const Demo = [1, 'hello', true]
type T3 = (typeof Demo)[number] // number | string | boolean

// as const 可以让 typeof 在获取类型的时候获取到字面量类型
const Demo = [1, 'hello', true] as const
type T4 = (typeof Demo)[number] // 1 | 'hello' | true
// 这在用 Record 构造对象时比较常见
// 比如要临时构造一个对象,key 取自数组 A,value 需要从接口返回的数组中获取那么可以这么做
const A = ['a', 'b', 'c'] as const
const temp = {} as Record<(typeof A)[number], any> // {a: xxx, b: xxx, c : xxx}

实际上,一般 keyof 和 typeof 一起配合使用的场景非常多,多加练习即可。

interface & type

TS 入门时多多少少都会对这两玩意儿有点困惑吧👻

  • interface
    • 自身只能表示 object/class/function 的类型
    • 同名的 interface 会自动聚合
  • type
    • 在 TS 中充当了别名的作用,几乎可以表示一切类型
    • 不能同名,但支持更复杂的类型操作

鉴于两者的特点:建议开发库时对外暴露的类型尽量使用 interface/class,方便使用者自行扩展,其他时候尽量使用 type 即可。

逆变 & 协变

协变(Covariance)和逆变(Contravariance)的概念用于描述类型系统中子类型和父类型之间的关系。

简单讲:

  • 协变,意味着子类型可以赋值给父类型
  • 逆变,意味着父类型可以赋值给子类型

在 TS 中特殊的,函数的参数是逆变的,函数的返回值是协变的

type Animal = {
  name: string
}
type Dog = Animal & { age: number }

let a!: () => Animal
let b!: () => Dog
a = b // ok
b = a // error

let c!: (param: Animal) => void 
let d!: (param: Dog) => void 
c = d // error
d = c // ok

TS 为了方便,把函数参数默认设置为了双变的,所以默认情况下,上面的 c = d 也是 ok 的。为了让类型系统更加准确,可以在 tsconfig 中配置 "strictFunctionTypes": true,这样函数参数只有逆变的特性了。

泛型 & infer

泛型需要注意的一个点是在进行约束时,如果泛型是联合类型,那么满足分配律

type P<T> = T extends 'X' ? 1 : 2
type T1 = P<'X' | 'Y'> // 1 | 2

type T2 = 'X' | 'Y' extends 'X' ? 1 : 2; // 2 直接进行约束则没有分配律

infer 用在 extends 之后,用来推断类型,是类型体操的关键字段之一:

type A<T> = T extends {a: infer U, b: infer U} ? U : any;
type B = A<{a: string, b: number}> // string | number

内置类型

熟练使用内置类型,会让体操事半功倍,比如我之前用 zustand 时,封装了一个 loading 函数:

/**
 * 用到了 Parameters & ReturnType
 * 使用的时候 const res = withLoading(apiService)(params), 
 * 得益于 ts 类型提示,上面的 apiService 的参数也提示到 params 的位置
 */
export const withLoading = <T extends (...args: any[]) => Promise<any>>(asyncFunction: T) => {
    const setLoading = useLoading.getState().setLoading
    return async (...args: Parameters<T>): Promise<ReturnType<T> | undefined> => {
        setLoading(true)
        const [err, res] = await till(asyncFunction(...args))
        setLoading(false)
        if (err) {
            console.error(err)
            return
        }
        return res
    }
}

最后,总而言之,言而总之 --- 菜就多练🤣

如果你有 TS 使用的小技巧,欢迎在下面的评论区留言👏🏻

参考