最近一直在看TS
,看的不少文章都是一些概念,并无什么实操,或者具体的场景。固然并非说概念不重要。知道与不知道之间是一道鸿沟,可是知道和如何用之间也一样是这样,因此我把这一个月学到的一些使用经验,总结为一篇文章,具体围绕一些场景进行思路的梳理。markdown
假设咱们要上传一个视频,根据分辨率分红了320p
、480p
、720p
、1080P
结构长这样。ssh
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
。ide
为何?由于Video
是一个联合类型,那么Video['urls']
一样也是一个联合类型,他们没有相同的键,因此是never函数
/** Video['urls'] = | { format320p: string } | { format480p: string } | { format720p: string } | { format1080p: string } */
type VideoType = keyof Video['urls'] // never
复制代码
那如何才能拿到咱们想要的键的联合呢,咱们来反推keyof
在什么状况下能够获得咱们想要的VideoType
ui
type VideoType = keyof {
format320p: string
format480p: string
format720p: string
format1080p: string
} // ok
复制代码
ok,这样是能够的,咱们再进一步推导url
type Urls =
& { format320p: string }
& { format480p: string }
& { format720p: string }
& { format1080p: string }
type VideoType = keyof Urls // ok
复制代码
哇哦~ 很棒!到这里咱们已经成功了一半了,咱们能够很清楚发现,相较以前的Video['urls']
,咱们只须要将联合类型转换成交叉类型就能够了。spa
咱们直接给出转换方法。code
type UnionToIntersection<T> =
(
T extends any
? (any: T) => any
: never
)
extends (any: infer Q) => any
? Q
: never
复制代码
要了解这段代码咱们须要两个前置条件:orm
猫
在什么状况下才是布偶猫
父类的哲学问题 -_-!!! )不了解这两个概念的小伙伴,请先看到这里,动手去搜搜相关的文章,去夯实基础,而后再回来继续通关!
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']>
复制代码
咱们先看一下这样的函数
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
}
}
复制代码
当咱们定义一个对象,而后经过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 复制代码