TypeScript学习、实战总结

1,883 阅读6分钟

前言

最近一直在看TS,看的很多文章都是一些概念,并没有什么实操,或者具体的场景。当然并不是说概念不重要。知道与不知道之间是一道鸿沟,但是知道和如何用之间也同样是这样,所以我把这一个月学到的一些使用经验,总结为一篇文章,具体围绕一些场景进行思路的梳理。

例子

场景一、联合类型转为交叉类型

假设我们要上传一个视频,根据分辨率分成了320p480p720p1080P结构长这样。

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

这时,如果我们想要通过推导得到类型VideoTypeformat320p | 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
  

要了解这段代码我们需要两个前置条件:

  1. 分布式条件类型
  2. 协变、逆变、双变、不变。(这是一个关于在什么情况下才是布偶猫父类的哲学问题 -_-!!! )

不了解这两个概念的小伙伴,请先看到这里,动手去搜搜相关的文章,去夯实基础,然后再回来继续通关!

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