乐趣区

关于javascript:精读Get-return-type-Omit-ReadOnly

解决 TS 问题的最好方法就是多练,这次解读 type-challenges Medium 难度 1~8 题。

精读

Get Return Type

实现十分经典的 ReturnType<T>

const fn = (v: boolean) => {if (v)
    return 1
  else
    return 2
}

type a = MyReturnType<typeof fn> // should be "1 | 2"

首先不要被例子吓到了,感觉必须执行完代码才晓得返回类型,其实 TS 曾经帮咱们推导好了返回类型,所以下面的函数 fn 的类型曾经是这样了:

const fn = (v: boolean): 1 | 2 => {...}

咱们要做的就是把函数返回值从外部抽出来,这非常适合用 infer 实现:

// 本题答案
type MyReturnType<T> = T extends (...args: any[]) => infer P ? P : never

infer 配合 extends 是解构简单类型的神器,如果对下面代码不能一眼了解,阐明对 infer 相熟度还是不够,须要多看。

Omit

实现 Omit<T, K>,作用恰好与 Pick<T, K> 相同,排除对象 T 中的 K key:

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {completed: false,}

这道题比拟容易尝试的计划是:

type MyOmit<T, K extends keyof T> = {[P in keyof T]: P extends K ? never : T[P]
}

其实依然蕴含了 descriptiontitle 这两个 Key,只是这两个 Key 类型为 never,不符合要求。

所以只有 P in keyof T 写进去了,前面怎么写都无奈将这个 Key 抹去,咱们应该从 Key 下手:

type MyOmit<T, K extends keyof T> = {[P in (keyof T extends K ? never : keyof T)]: T[P]
}

但这样写依然不对,咱们思路正确,即把 keyof T 中归属于 K 的排除,但因为前后 keyof T 并没有关联,所以须要借助 Exclude 通知 TS,前后 keyof T 是同一个指代(上一讲实现过 Exclude):

// 本题答案
type MyOmit<T, K extends keyof T> = {[P in Exclude<keyof T, K>]: T[P]
}

type Exclude<T, U> = T extends U ? never : T

这样就正确了,把握该题的外围是:

  1. 三元判断还能够写在 Key 地位。
  2. JS 抽不抽函数成果都一样,但 TS 须要推断,很多时候抽一个函数进去就是为了通知 TS“是同一指代”。

当然既然都用上了 Exclude,咱们不如再联合 Pick,写出更优雅的 Omit 实现:

// 本题优雅答案
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

Readonly 2

实现 MyReadonly2<T, K>,让指定的 Key K 成为 ReadOnly:

interface Todo {
  title: string
  description: string
  completed: boolean
}

const todo: MyReadonly2<Todo, 'title' | 'description'> = {
  title: "Hey",
  description: "foobar",
  completed: false,
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
todo.completed = true // OK

该题乍一看蛮难的,因为 readonly 必须定义在 Key 地位,但咱们又没法在这个地位做三元判断。其实利用之前咱们本人做的 PickOmit 以及内置的 Readonly 组合一下就进去了:

// 本题答案
type MyReadonly2<T, K extends keyof T> = Readonly<Pick<T, K>> & Omit<T, K>

即咱们能够将对象一分为二,先 PickK Key 局部设置为 Readonly,再用 & 合并上剩下的 Key,正好用到上一题的函数 Omit,完满。

Deep Readonly

实现 DeepReadonly<T> 递归所有子元素:

type X = { 
  x: { 
    a: 1
    b: 'hi'
  }
  y: 'hey'
}

type Expected = { 
  readonly x: { 
    readonly a: 1
    readonly b: 'hi'
  }
  readonly y: 'hey' 
}

type Todo = DeepReadonly<X> // should be same as `Expected`

这必定须要用类型递归实现了,既然要递归,必定不能依赖内置 Readonly 函数,咱们须要将函数开展手写:

// 本题答案
type DeepReadonly<T> = {readonly [K in keyof T]: T[K] extends Object> ? DeepReadonly<T[K]> : T[K]
}

这里 Object 也能够用 Record<string, any> 代替。

Tuple to Union

实现 TupleToUnion<T> 返回元组所有值的汇合:

type Arr = ['1', '2', '3']

type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'

该题将元组类型转换为其所有值的可能汇合,也就是咱们心愿用所有下标拜访这个数组,在 TS 里用 [number] 作为下标即可:

// 本题答案
type TupleToUnion<T extends any[]> = T[number]

Chainable Options

间接看例子比拟好懂:

declare const config: Chainable

const result = config
  .option('foo', 123)
  .option('name', 'type-challenges')
  .option('bar', { value: 'Hello World'})
  .get()

// expect the type of result to be:
interface Result {
  foo: number
  name: string
  bar: {value: string}
}

也就是咱们实现一个绝对简单的 Chainable 类型,领有该类型的对象能够 .option(key, value) 始终链式调用上来,直到应用 get() 后拿到聚合了所有 option 的对象。

如果咱们用 JS 实现该函数,必定须要在以后闭包存储 Object 的值,而后提供 get 间接返回,或 option 递归并传入新的值。咱们无妨用 Class 来实现:

class Chain {constructor(previous = {}) {this.obj = { ...previous}
  }
  
  obj: Object
  get () {return this.obj}
  option(key: string, value: any) {
    return new Chain({
      ...this.obj,
      [key]: value
    })
  }
}

const config = new Chain()

而本地要求用 TS 实现,这就比拟乏味了,正好比照一下 JS 与 TS 的思维。先打个岔,该题用下面 JS 形式写进去后,其实类型也就进去了,但用 TS 残缺实现类型也另有其用,特地在一些简单函数场景,须要用 TS 零碎形容类型,JS 真正实现时拿到 any 类型做纯运行时解决,将类型与运行时候来到。

好咱们回到题目,咱们先把 Chainable 的框架写进去:

type Chainable = {option: (key: string, value: any) => any
  get: () => any}

问题来了,如何用类型形容 option 后还能够接 optionget 呢?还有更麻烦的,如何一步一步将类型传导上来,让 get 晓得我此时拿的类型是什么呢?

Chainable 必须接管一个泛型,这个泛型默认值是个空对象,所以 config.get() 返回一个空对象也是正当的:

type Chainable<Result = {}> = {option: (key: string, value: any) => any
  get: () => Result}

下面的代码对于第一层是齐全没问题的,间接调用 get 返回的就是空对象。

第二步解决递归问题:

// 本题答案
type Chainable<Result = {}> = {option: <K extends string, V>(key: K, value: V) => Chainable<Result & {[P in K]: V
  }>
  get: () => Result}

递归思维大家都懂就不赘述了。这里有个看似不值得一提,但的确容易坑人的中央,就是如何形容一个对象仅蕴含一个 Key 值,这个值为泛型 K 呢?

// 这是错的,因为形容了一大堆类型
{[K] : V
}

// 这也是错的,这个 K 就是字面量 K,而非你心愿的类型指代
{K: V}

所以必须应用 TS“习惯法”的 [K in keyof T] 的套路形容,即使咱们晓得 T 只有一个固定的类型。可见 JS 与 TS 齐全是两套思维形式,所以精通 JS 不必然精通 TS,TS 还是要大量刷题造就思维的。

Last of Array

实现 Last<T> 获取元组最初一项的类型:

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type tail1 = Last<arr1> // expected to be 'c'
type tail2 = Last<arr2> // expected to be 1

咱们之前实现过 First,相似的,这里无非是解构时把最初一个形容成 infer

// 本题答案
type Last<T> = T extends [...infer Q, infer P] ? P : never

这里要留神,infer Q 有人第一次可能会写成:

type Last<T> = T extends [...Others, infer P] ? P : never

发现报错,因为 TS 里不可能轻易应用一个未定义的泛型,而如果把 Others 放在 Last<T, Others> 里,你又会面临一个 TS 大难题:

type Last<T, Others extends any[]> = T extends [...Others, infer P] ? P : never

// 必然报错
Last<arr2>

因为 Last<arr2> 仅传入了一个参数,必然报错,但第一个参数是用户给的,第二个参数是咱们推导进去的,这里既不能用默认值,又不能不写,无解了。

如果真的硬着头皮要这么写,必须借助 TS 还未通过的一项个性:局部类型参数推断,举个例子,很可能当前的语法是:

type Last<T, Others extends any[] = infer> = T extends [...Others, infer P] ? P : never

这样首先传参只须要一个了,而且还申明了第二个参数是一个推断类型。不过该提案还未反对,而且实质上和把 infer 写到表达式外面含意和成果也都一样,所以对这道题来说就不必折腾了。

Pop

实现 Pop<T>,返回去掉元组最初一项之后的类型:

type arr1 = ['a', 'b', 'c', 'd']
type arr2 = [3, 2, 1]

type re1 = Pop<arr1> // expected to be ['a', 'b', 'c']
type re2 = Pop<arr2> // expected to be [3, 2]

这道题和 Last 简直齐全一样,返回第一个解构值就行了:

// 本题答案
type Pop<T> = T extends [...infer Q, infer P] ? Q : never

总结

从题目中很显著能看出 TS 思维与 JS 思维有很大差别,想要真正把握 TS,大量刷题是必须的。

探讨地址是:精读《Get return type, Omit, ReadOnly…》· Issue #422 · dt-fe/weekly

如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>

版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)

退出移动版