类型的智能提示
类型约束是类型提示的基础,正因为有了类型约束,类型提示才能正常工作。类型约束越严谨,类型提示就越精准。而定义一个严谨的类型约束离不开泛型。下面我们通过几个例子,来了解下泛型在类型约束中的作用。
可以在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有几个需求:
- 第一个参数需要智能提示
- 第二个参数的类型要符合这个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…