前言
最近一直在看TS
,看的很多文章都是一些概念,并没有什么实操,或者具体的场景。当然并不是说概念不重要。知道与不知道之间是一道鸿沟,但是知道和如何用之间也同样是这样,所以我把这一个月学到的一些使用经验,总结为一篇文章,具体围绕一些场景进行思路的梳理。
例子
场景一、联合类型转为交叉类型
假设我们要上传一个视频,根据分辨率分成了320p
、480p
、720p
、1080P
结构长这样。
type Format320 = { urls: { format320p: string } }
type Format480 = { urls: { format480p: string } }
type Format720 = { urls: { format720p: string } }
type Format1080 = { urls: { format1080p: string } }
// 视频类型
type Video = Format1080 | Format720 | Format480 | Format320
这时,如果我们想要通过推导得到类型VideoType
:format320p | format480p | format720p | format1080p
。应该如何做呢?
有人可能会说很简单直接keyof Video['urls']
就可以了,这样其实是不行的,获取到的结果是never
。
为什么?因为Video
是一个联合类型,那么Video['urls']
同样也是一个联合类型,他们没有相同的键,所以是never
/**
Video['urls'] =
| { format320p: string }
| { format480p: string }
| { format720p: string }
| { format1080p: string }
*/
type VideoType = keyof Video['urls'] // never
那如何才能拿到我们想要的键的联合呢,我们来反推keyof
在什么情况下可以得到我们想要的VideoType
type VideoType = keyof {
format320p: string
format480p: string
format720p: string
format1080p: string
} // ok
ok,这样是可以的,我们再进一步推导
type Urls =
& { format320p: string }
& { format480p: string }
& { format720p: string }
& { format1080p: string }
type VideoType = keyof Urls // ok
哇哦~ 很棒!到这里我们已经成功了一半了,我们可以很清楚发现,相较之前的Video['urls']
,我们只需要将联合类型转换成交叉类型就可以了。
我们直接给出转换方法。
type UnionToIntersection<T> =
(
T extends any
? (any: T) => any
: never
)
extends (any: infer Q) => any
? Q
: never
要了解这段代码我们需要两个前置条件:
- 分布式条件类型
- 协变、逆变、双变、不变。(这是一个关于
猫
在什么情况下才是布偶猫
父类的哲学问题 -_-!!! )
不了解这两个概念的小伙伴,请先看到这里,动手去搜搜相关的文章,去夯实基础,然后再回来继续通关!
ok,那么我们继续。这个类型的关键点在于第二个条件。由于推断infer Q
是参数,位于逆变位,TS为了兼容性会将|
转换为&
。又因为第一个条件类型extends any
所有的类型都可以通过,联合类型别转换为了(any: T) => any
,恰好被同样是参数
的infer Q
推断出来,这样就从原本的联合类型转变成了交叉类型。
完整代码如下。
type Format320 = { urls: { format320p: string } }
type Format480 = { urls: { format480p: string } }
type Format720 = { urls: { format720p: string } }
type Format1080 = { urls: { format1080p: string } }
// 视频类型
type Video = Format1080 | Format720 | Format480 | Format320
type UnionToIntersection<T> =
(T extends any ? (arg: T) => any : never)
extends (arg: infer Q) => any
? Q
: never
type VideoType = keyof UnionToIntersection<Video['urls']>
场景二、hasOwnProperty
我们先看一下这样的函数
function print(person: object) {
if (typeof person === 'object'
&& person.hasOwnProperty('name')
) {
console.log(person.name) // error, object不存在name属性
}
}
这是经典的“我知道,但是TS不知道”的案例,虽然我们通过hasOwnProperty
进行判断是否有name
属性,但是TS并不清楚,那我们要怎么办呢?
这里,我们需要两个概念,一个是类型谓词
,另一个是交叉类型
。
ok,我们来写一个函数,这个函数可以判断参数是不是object
,如果是的话,我们再通过交叉类型
给这个参数重新键入(原文为retype
,英文好的自行理解)新的类型。
function hasOwnProperty<
Obj extends object,
Key extends PropertyKey
>(obj: Obj, key: Key): obj is Obj & Record<Key, unknow> {
return obj.hasOwnProperty(key)
}
这里的关键是obj is Obj & Record<Key, unknow>
,由于obj is Obj
永远为true
,当结果为true
时TS允许我们进行重新键入类型,这里我们通过交叉类型
为类型添加了对应的Key
属性,这样再之后的取值就不会报错了。颇费特~
完整代码
function hasOwnProperty<
Obj extends object,
Key extends PropertyKey
>(obj: Obj, key: Key): obj is Obj & Record<Key, unknow> {
return obj.hasOwnProperty(key)
}
function print(person: object) {
if (typeof person === 'object'
&& hasOwnProperty(person, 'name')
// 这里的person类型变成了object & {name: unknow }
&& typeof person.name === 'string'
) {
console.log(person.name) // ok
}
}
场景三、DefineProperty
当我们定义一个对象,然后通过Object.defineProperty
去给它赋值的时候,这时TS是不知道的。
let storage = {
maNumber: 99
}
Object.defineProperty(storage, 'number', {
configurable: true,
writable: true,
enumberable: true,
value: 10,
})
console.log(storage.number) // error! 不存在number属性
是不是似曾相识的场景,和场景二
一样,我们还是需要通过类型谓词重新键入类型。
但是与场景二
不同的是,这次我们需要考虑一些错误情况,这里我们需要的前置知识为asserts 语句
。
我们首先先实现一下defineProperty
方法
function defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor
>(obj: Obj, key: Key, desc: PDesc):
asserts obj is Obj & DefineProperty<Key, PDesc> {
Object.definePropety(obj, key, desc)
}
ok,同样的套路,先用个必然为true
的类型谓词,然后再来个交叉类型。现在我们只要实现了DefineProperty
就好了。
那我们为什么要用aseerts
呢?我们知道对象的值有两种模式,一种是value
值的模式,另一种是存取器getter/setter
,如果我们在descriptor
中同时定义了这两种类型,JS是会报错的,所以我们要通过TS来预检测。
我们先实现一下DefineProperty
type DefineProperty<
Key extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { writable: any, set(val: any): any } ? never :
Desc extends { writable: any, get(): any } ? never :
Desc extends { writable: false } ? ReadOnly<InferValue<Key, Desc>> :
Desc extends { writable: true } ? InferValue<Key, Desc> :
ReadOnly<InferValue<Key, Desc>>
诶~ 道理我都懂,这个InferValue
是个啥,当然是我们接下来要实现的类型了!
我们回过头来看一下我们希望DefineProperty
返回个什么东西,根据场景二
的经验,我们需要的是个Record<Key, Value>
。又因为值有两种类型,那我们通过InferValue
进行推断类型就好了。
type InferValue<
Key extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { value: any, get(): any } ? never :
Desc extends { value: infer T } ? Record<Key, T> :
Desc extends { get(): infer T } ? Record<Key, T> :
never
大功告成!完整代码如下
function defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor
>(obj: Obj, key: Key, desc: PDesc):
asserts obj is Obj & DefineProperty<Key, PDesc> {
Object.definePropety(obj, key, desc)
}
type DefineProperty<
Key extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { writable: any, set(val: any): any } ? never :
Desc extends { writable: any, get(): any } ? never :
Desc extends { writable: false } ? ReadOnly<InferValue<Key, Desc>> :
Desc extends { writable: true } ? InferValue<Key, Desc> :
ReadOnly<InferValue<Key, Desc>>
type InferValue<
Key extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { value: any, get(): any } ? never :
Desc extends { value: infer T } ? Record<Key, T> :
Desc extends { get(): infer T } ? Record<Key, T> :
never
let storage = { maxValue: 20 }
defineProperty(storage, 'number', 123)
console.log(storage.number) // ok
练习题
最后,给大家来一到经典练习题,ssh昊神
也发过对于这道题的解题思路,我把答案放在底下,大家可以先自己尝试尝试,看看能不能写出来。
/**
*
declare function dispatch(arg: Action): void
dispatch({
type: 'LOGIN',
emialAddress: string
})
转变成
dispatch('LOGIN', {
emialAddress: string
})
*/
type Action =
| {
type: 'INIT'
}
| {
type: 'SYNC'
}
| {
type: 'LOG_IN',
emialAddress: string
}
| {
type: 'LOG_IN-SUCCESS',
accessToken: string
}
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
答案
type Action =
| {
type: 'INIT'
}
| {
type: 'SYNC'
}
| {
type: 'LOG_IN',
emialAddress: string
}
| {
type: 'LOG_IN-SUCCESS',
accessToken: string
}
declare function dispatch<T>(
type: T,
action:
): void
type ActionType = Action['type']
// 找到对应的type
type ExtraAction<A, T> = A extends {type: T} ? A : never
// 去除type属性
type ExcludeTypeField<A> = {[K in Exclude<keyof A, 'type'>]: A[K]}
// 组合在一起
type ExtractActionParameterWithoutType<A, T> = ExcludeTypeField<ExtraAction<A, T>>
type ExtractSimpleAction<T> = T extends any
? {} extends ExcludeTypeField<T>
? T
: never
: never
type SimpleActionType = ExtractSimpleAction<Action>['type']
type ComplexActionType = Exclude<ActionType, SimpleActionType>
// 重载
declare function dispatch<T extends SimpleActionType>(type: T): void
declare function dispatch<T extends ComplexActionType>(
type: T,
action: ExtractActionParameterWithoutType<Action, T>
): void