TypeScript泛型的实际应用

1,159 阅读4分钟

类型的智能提示

类型约束是类型提示的基础,正因为有了类型约束,类型提示才能正常工作。类型约束越严谨,类型提示就越精准。而定义一个严谨的类型约束离不开泛型。下面我们通过几个例子,来了解下泛型在类型约束中的作用。

可以在TypeScript Playground测试下方代码

松弛的类型约束

function extract(source: any, key: string) {
  return source[key]
}
// 1. extract的第二个参数只提示到string类型为止 【问题】
// 2. n会被推导为any,但是根据我们传入的source数据,n只可能是number 【问题】
const n = extract({foo: 1}, '')

这样,不仅智能提示不友好,在类型安全上也存在隐患

普通的类型约束

function extract<S>(source: S, key: keyof S) {
  return source[key]
}

const source = {foo: 1, bar: '2'}
// 在这里,<S>会被自动推导为source的类型,所以我们不需要手动去写
const n = extract(source, 'foo')

现在我们约束key为keyof S也就是S这个泛型的所有key的取值,这样智能提示就可以在输入extract第2个参数的时候展示一个key值列表。

但是,依然存在一个问题:上述代码中,n会被推导为string | number,但是我们传入的第二个参数'foo'对应的值是number,有没有办法让n被推导为number呢?

严谨的类型约束

type Source = {
  [key in string]: any
}

function extract<S, K extends keyof S>(source: S, key: K) {
  return source[key]
}

const source = {foo: 1, bar: '2'}
// 在这里,<S, K>会被自动推导为source和'foo'的类型,所以我们不需要手动去写
const n = extract(source, 'foo')

这样,n就会根据所传入的key的不同而推导为正确的类型。比如传foo,则推导为number;传入bar,则推导为string

分析

从"开始严谨的类型约束"到"更加严谨的类型约束"发生了如下变化:

// 旧
function extract<S>(source: S, key: keyof S) {}
// 新
function extract<S, K extends keyof S>(source: S, key: K) {}

可以看到,我们只是把keyof S移动到了泛型定义中,那么为什么这么做就能让extract的返回值的类型被准确被推导呢?

其实这种用法在TypeScript中称为:Generic Constraints,即泛型约束,它可以带来更加精准的类型约束。在旧的extract写法中,extract的返回值被约束为S[keyof S],而在新的extract写法中则收缩至S[K],这样TypeScript编译器就能精准地根据key的取值去推断类型。

类型的关联性约束

类型约束本身很简单,只要给每个参数一个类型,这样就能针对每个参数进行类型约束。但是,如果要做到参数之间的相互约束,就需要借助泛型了。其实上方所讲的"严谨的类型约束"就是一个很好的例子:extract函数有两个参数source和key,key的类型受source类型的影响,存在一定的关联性。现在我们通过一个更复杂的例子,详细介绍一些泛型在类型关联性约束场景下的作用。

类Redux的局部状态管理工具

随着Vue3 composition-api的使用,业务逻辑和状态正逐渐去中心化,原始的中心化的vuex变得不再合适。很多人已经开始使用reactive创建一个简易型的store,来完成局部逻辑下的状态管理。然而,过于灵活的局部状态用导致维护上的灾难,因此我们需要一定的规范来约束修改store的过程,通过dispatch一个action来修改store是一个不错的方式,类似这样:

const store = {
  name: '',
  age: 0
}
const dispatch = useDispatch(store, {
  changeName: (store, payload: string) => {
  	store.name = payload
  },
  changeAge: (store, payload: number) => { 
    store.age = payload
  }
})
dispatch('changeName', 'fxxjdedd')

function useDispatch() { ... }

在这里我们对dispatch有几个需求:

  1. 第一个参数需要智能提示
  2. 第二个参数的类型要符合这个reducer中payload的类型定义

为了实现这两个需求,useDispatch必须要对store和reducers这两个参数添加关联性约束。

实现

可以在TypeScript Playground测试下方代码

const store = {
  name: '',
  age: 0
}
const dispatch = useDispatch(store, {
  changeName: (store, payload: string) => {
  	store.name = payload
  },
  changeAge: (store, payload: number) => { 
    store.age = payload
  }
})

dispatch('changeName', 'fxxjdedd')

// ================以下是实现================

type ReducerHandler<S, P> = (store: S, payload: P) => void

type Reducers<S> = { [key in string]: ReducerHandler<S, any> }

type Actions<R> = keyof R
// 使用infer提取类型,即把ReducerHandler第二个泛型的类型提取出来作为payload的类型
type PayloadOfHandler<R, Act extends Actions<R>> = R[Act] extends ReducerHandler<any, infer P> ? P : never 

function useDispatch<S, R extends Reducers<S>>(store: S, reducers: R) {
    return function<Act extends Actions<R>>(action: Act, payload: PayloadOfHandler<R, Act>) { 
        reducers[action](store, payload)
    }
}

上述需求的更详细的实现我单独写了个库,地址在这里:github.com/fxxjdedd/vt…