TypeScript体操运动员🤸类型体操记录ing......

695 阅读17分钟

刷题前的准备说明

打开类型体操官网,在下面的README里随便找道题点击打开之后,选择take the challenge之后会跳转到的ts playground,这里已经搭建好了做题需要的东西:

/* _____________ Your Code Here _____________ */

// type If<C extends boolean, T, F> = C extends true ? T : F;
type If<C , T, F> = C extends true ? T : F;
// type If<C, T, F> = any;


/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<If<true, 'a', 'b'>, 'a'>>,
  Expect<Equal<If<false, 'a', 2>, 2>>,
]

// @ts-expect-error
type error = If<null, 'a', 'b'>

在your code here部分是你需要实现的类型编程。

Test Cases部分表示测试用例,你编写的类型需要通过全部的测试用例。Equal<If<true, 'a', 'b'>, 'a'>中Equal就表示相等,也就是上面题目中If类型实现的功能期望的结果和'a'相等。@ts-expect-error下的type error表示这样用应该是错误的,应该飘红。未正确实现前,type cases里的用例和@ts-expect-error这一行都是飘红线的(报错),正确实现之后是没有任何红线的。

easy

1.Pick

题目:实现TS内置的泛型功能Pick<T, K>,Pick的功能就是可以从类型T中选取属性K

/* _____________ Your Code Here _____________ */
type MyPick<T, K> = any

// 实现
type MyPick<T, K extends keyof T> = {
  [P in K] : T[P]
}

// 举例
interface Todo {
  title: string
  description: string
  completed: boolean
}
let pick1: MyPick<Todo, 'title' | 'completed'>; //此时的pick1所得到的类型和下面一致
interface Expected2 {
  title: string
  completed: boolean
}

MyPick<Todo, 'invalid'> // 报错,因为invalid不是Todo里的属性

分析

先看题目:从类型T中选取属性K。我们可以得出:

  1. 从T中找到所有属性 → keyof T 取到T的所有属性(这里的属性可以简单理解为对象的键名)
  2. K是在T的所有属性之中的某一些 → extends表示约束:K extends keyof T
  3. 因为属性K(多个)是从类型T中选取的,所以选出的属性应该和在T中是一样的 → P in K 可以理解为for...in...循环里的in,从集合K中取单个P。

知识点:keyof、extends、in

2.Readonly

实现内置工具类型Readonly,这个类型就是将T中的所有属性变为只读。

/* _____________ Your Code Here _____________ */
type MyReadonly<T> = any

// 实现
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}

// 举例
interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

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

分析

看题目:要使得每个属性都变成只读,需要用到readonly关键字。得出步骤:

  1. 先从T中拿到所有属性 → keyof T
  2. 取出每个属性 → P in keyof T,属性的类型不变所以T[P]
  3. 让属性变为只读 → 在属性前面加上readonly

知识点:readonly、keyof、in

3.Tuple to Object

给定一个数组,将它转换为一个对象类型,并且这个对象类型里的键/值都必须是数组的元素。

// 举例
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

/* _____________ Your Code Here _____________ */

type TupleToObject<T extends readonly any[]> = any

// 解答
type TupleToObject<T extends readonly any[]> = {
   [P in T[number]]: P;
}

分析

分析题目:数组转换为对象,键值对都是数组的元素

  1. 拿出数组的所有元素 → 索引类型里T[number]表示取出所有数组元素
  2. 遍历元素,拿到每一个 → [P in T[number]]表示每一个
  3. 将每个数组元素赋值写成对象的键值对形式 → P即是元素的值,键值对赋值[P in T[number]]: P即可

知识点:索引类型、in、Tuple

Attention

Tuple是数组的一种,它的特点就是确切地知道包含了多少个元素,以及每个位置元素的类型也是确定的。

type StringNumberPair = [string, number];

StringNumberPair就是一个Tuple类型,它不仅限定了数组的0号位必须是string、1号位必须是number之外,还限制了数组只能有两个元素

我们取tuple和Array的length属性,可以看出区别。

type arr = any[];

type a = StringNumberPair['length']; // a = 2
type b = arr['length']; // a = number

tuple还有一个小特性,可以像JS解构数组一样把tuple里的元素解构出来。

function doSomething(stringHash: [string, number]) {
    const [inputString, hash] = stringHash;
    // inputString:string
    // hash:number
}

4.First of Array

实现一个通用First<T>,它接受一个数组T并返回它的第一个元素的类型。

// 示例
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

/* _____________ Your Code Here _____________ */
type First<T extends readonly any[]> = any;

// 解答
type First<T extends readonly any[]> = T extends [] ? never : T[0];
type First<T extends readonly any[]> = T['length'] extends 0 ? never : T[0];
type First<T extends readonly any[]> = T extends {length: 0} ? never : T[0];

分析

这道题目的很简单,主要是第三个测试用例当是空数组的时候要返回never类型,当数组有值的时候就原路返回。上面的三种答案第一种算是比较符合JS的思想的,后面两种有点异曲同工之妙,都是利用了数组的length属性。

  1. type First<T extends any[]> = T[0]写出这个比较容易,但是过不了never的测试用例。
  2. 空数组为never,非空返回第一个 -> 条件类型没跑了 A?B:C
  3. 怎么判断是否为空数组:(1)T extends [];(2)根据length属性,T['length']类似JS的用法拿到T的长度,长度extends 0也是空数组;(3)数组也是对象,T自带的length{length: 0}类比一下也能判断。
  4. 最后大家仔细观察给出的示例T extends readonly any[],在数组前加了readonly。这也涉及到了元组的特点:元组是数组的一种,元组可以确切地知道包含多少个元素,以及每个位置都是什么类型。TS handbook Tuple部分

5.Length of Tuple

题目:取元组的length

type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

type teslaLength = Length<tesla>  // expected 4
type spaceXLength = Length<spaceX> // expected 5

/* _____________ Your Code Here _____________ */
type Length<T extends any> = any;

// 解答
type Length<T extends readonly any[]> = T['length'];

分析

这道题比较简单,上一题搞懂元组的特性就明白了。至于T['length']和JS一模一样。

6.Exclude

题目:

实现TS内置工具函数Exclude<T, U>:从联合类型T中排除U的类型成员,来构造一个新的类型。

/* _____________ Your Code Here _____________ */
type MyExclude<T, U> = any;
/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
    Expect<Equal<MyExclude<"a" | "b" | "c", "a">, Exclude<"a" | "b" | "c", "a">>>,
    Expect<Equal<MyExclude<"a" | "b" | "c", "a" | "b">, Exclude<"a" | "b" | "c", "a" | "b">>>,
    Expect<Equal<MyExclude<string | number | (() => void), Function>, Exclude<string | number | (() => void), Function>>>,
]

分析

T类型里去剔除U类型中的属性,那我们只需要让T中的属性一个个去和U比较,不属于U的就留下,属于U的用never类型代替。

这道题需要用到分布式条件类型的知识,这是属于TS的新知识点。

// 解答
type MyExclude<T, U> = T extends U ? never : T;

浅浅回顾一次分布式条件类型: 当条件类型作用于泛型类型时,当传入泛型的是联合类型(union)时,这个时候条件类型会成为分布式类型。我们看测试用例,刚好是union,这个时候的T extends U的效果刚好就是T中每一个元素去和U比较。

7.Awaited

题目:假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。

比如:Promise<ExampleType>,请你返回 ExampleType 类型。

/* _____________ 你的代码 _____________ */

type MyAwaited = any


/* _____________ 测试用例 _____________ */
import { Equal, Expect } from '@type-challenges/utils'

type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>

type cases = [
  Expect<Equal<MyAwaited<X>, string>>,
  Expect<Equal<MyAwaited<Y>, { field: number }>>,
  Expect<Equal<MyAwaited<Z>, string | number>>,
]

// @ts-expect-error
type error = MyAwaited<number>

@ts-expect-error注释表示当type error = MyAwaited<number> 使用时会报错。

分析

  1. 首先要传入类型的话,我们需要在类型后面这样写MyAwaited<T>
  2. 其次,为了让type error = MyAwaited<number>报错,我们需要让泛型T被约束为Promise,MyAwaited<T>变成MyAwaited<T extends Promise<any>>;
  3. 最后写取出Promise里类型的逻辑:
type MyAwaited<T extends Promise> =  A extends Promise<infer B> ? B :never

上面涉及到了infer的知识点和条件类型的知识点,我们想要Promise的传入东西,用infer表示推断。但是这个并不是完全正确的答案,因为MyAwaited<Promise<string | number>>会直接得到Promise<string | number>类型,并不会得到string | number

所以还需要稍加修改:

type MyAwaited<T extends Promise> =  
    A extends Promise<infer B> ? 
    B extends Promise<any> ? MyAwaited<B> : B
    :never

再递归判断一下里面是不是嵌套的Promise就行了。

8.If

题目:实现If类型If<C, T, F>,根据你在C位置输入true/false来决定输出T或者F类型。

/* _____________ Your Code Here _____________ */

type If<C, T, F> = any;


/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<If<true, 'a', 'b'>, 'a'>>,
  Expect<Equal<If<false, 'a', 2>, 2>>,
]

// @ts-expect-error
type error = If<null, 'a', 'b'>

这道题首先想到的条件类型:

type If<C, T, F> = C extends true ? T : F;

但是这里有个小点需要注意, 看测试用例 @ts-expect-error下,C传入null是需要报错的,上面并不会报错,因为我们并没有限制类型C只能为boolean类型。

修改后:

type If<C extends boolean, T, F> = C extends true ? T : F;

9.Concat

题目:在类型系统里实现 JavaScript 内置的 Array.concat 方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。

/* _____________ Your Code Here _____________ */

type Concat<T, U> = any;

/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Concat<[], []>, []>>,
  Expect<Equal<Concat<[], [1]>, [1]>>,
  Expect<Equal<Concat<[1, 2], [3, 4]>, [1, 2, 3, 4]>>,
  Expect<Equal<Concat<['1', 2, '3'], [false, boolean, '4']>, ['1', 2, '3', false, boolean, '4']>>,
]

分析

这道题其实很简单,只需要知道TS也有JS的...运算符(Rest Parameters),且用法一致。TS handbook对...的解释也是引用了JS在MDN的解释

type Concat<T extends any[], U extends any[]> = [...T, ...U];

10.Includes

题目:在类型系统里实现 JavaScript 的 Array.includes 方法,这个类型接受两个参数,返回的类型要么是 true 要么是 false

/* _____________ 你的代码 _____________ */

type Includes<T extends readonly any[], U> = any;


/* _____________ 测试用例 _____________ */
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'>, true>>,
  Expect<Equal<Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'>, false>>,
  Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 7>, true>>,
  Expect<Equal<Includes<[1, 2, 3, 5, 6, 7], 4>, false>>,
  Expect<Equal<Includes<[1, 2, 3], 2>, true>>,
  Expect<Equal<Includes<[1, 2, 3], 1>, true>>,
  Expect<Equal<Includes<[{}], { a: 'A' }>, false>>,
  Expect<Equal<Includes<[boolean, 2, 3, 5, 6, 7], false>, false>>,
  Expect<Equal<Includes<[true, 2, 3, 5, 6, 7], boolean>, false>>,
  Expect<Equal<Includes<[false, 2, 3, 5, 6, 7], false>, true>>,
  Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>,
  Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>,
  Expect<Equal<Includes<[1], 1 | 2>, false>>,
  Expect<Equal<Includes<[1 | 2], 1>, false>>,
  Expect<Equal<Includes<[null], undefined>, false>>,
  Expect<Equal<Includes<[undefined], null>, false>>,
]

分析

先看测试用例,就知道这个Include需要考虑的情况比较多。这道题的首要思路和之前的Exclude差不多,就是把T里的每一个和U比较进行判断,但是不同的是Exclude不是数组,而是一个Union,比较好去做判断。 这道题写JS是比较好写的,因为JS里有===运算符,但是TS的type里是没有这个的。

先看JS的实现方式:

function MyIncludes(arr, item) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === item) {
      return true;
    }
  }
  return false;
}

根据JS代码我们总结出三个步骤:

  1. 循环遍历得到arr的每个arr的每个值
  2. 将arr的每个值依次与目标item进行===判断
  3. 做if else判断,来返回true/false。

然后我们用TS来思考🤔转换JS代码应该怎么做:

第一步,循环遍历arr有两种方式:首先是利用数组的特性T[number]取到每个值;另一种方式是利用...运算符和infer直接去表示一个数组T extends readonly [infer A, ...infer B]

第二步,我们先尝试A extends B来做===判断,看看A extends B与JS里的===有没有区别。

第三步,做判断的话TS里只有条件类型里的三元运算符A ? B : c

综上,根据三步我们得出下面的结果:

type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;

这个时候会发现通过了大部分测试的case,有小部分通过不了:

  Expect<Equal<Includes<[{}], { a: 'A' }>, false>>,
  Expect<Equal<Includes<[boolean, 2, 3, 5, 6, 7], false>, false>>,
  Expect<Equal<Includes<[true, 2, 3, 5, 6, 7], boolean>, false>>,
  Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>,
  Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>,
  Expect<Equal<Includes<[1], 1 | 2>, false>>,
  Expect<Equal<Includes<[1 | 2], 1>, false>>,

比如第一个没通过的case,在JS里{ a: 'A' } === {}肯定是不等的,返回false是所期望的,但是在TS里{ a: 'A' } extends {}确实正确✅的,所以第一个case会返回true。包括后面的几个case都是因为extends的原因,false extends boolean也是毛病的。这里说明TS里A extends B和JS里的===还是不一样的。

这里主要说一对特别有意思的case:

  Expect<Equal<Includes<[1], 1 | 2>, false>>,
  Expect<Equal<Includes<[1 | 2], 1>, false>>,

上面第一个case里Includes返回的是boolean不是true也不是false,第二个case返回的是true。第二个很好解释:1 extends 1|2很明显是返回true。第一个为什么返回boolean呢:1|2 extends 1这难道不是false吗,其实是因为在条件类型里这里变为分布式条件类型判断了,union里的每一个type会去和extends后的type进行比较,也就是1 extends 1 | 1 extends 2进而得到true | false,这就是boolean。所以这个具体返回的值会根据你的输入而变化。

好了,回到这道题本身,既然extends不能代替===,TS里也没有现成的语法供我们使用,那我们就需要自己去实现一个相等的泛型Equal去表示===

type Equal<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

这个Equal我是直接把utility-types库里的Equal工具函数直接搬过来的,并且我是真的看不懂为什么这么实现。(先埋个坑

回到题目,如果要使用Equal的话第一步使用U extends T[number]就没法用了,所以第一步我们选择另一种取数组元素的方式[infer A, ...infer B], 这种形式在JS里需要这样实现:

function MyIncludes([A, ...B], item) {
  if (A === item) {
    return true;
  } else {
    MyIncludes(B, item); 
  }
  return false;
}

利用...运算符,每次都去比较数组的第一个是不是和item相等。那么,我们将上面的JS转化为TS:

type Includes<T extends readonly any[], U> = T extends [infer A, ...infer B]
  ? Equal<A, U> extends true ? true : Includes<B, U>
  : false

11.Push

题目:实现Array.push功能。

* _____________ Your Code Here _____________ */

type Push<T, U> = any
type cases = [
  Expect<Equal<Push<[], 1>, [1]>>,
  Expect<Equal<Push<[1, 2], '3'>, [1, 2, '3']>>,
  Expect<Equal<Push<['1', 2, '3'], boolean>, ['1', 2, '3', boolean]>>,
]

JS里的push大家都比较熟悉,这道题完全可以用JS的思路来实现。利用...rest运算符。

type Push<T extends any[], U> = [...T, U]

做到这里其实举一反三就能写完下一道题,实现Array.unshift功能:

type Unshift<T extends any[], U> = [U, ...T]

12.Paramater

题目:实现TS内置工具函数Paramater:将函数类型的参数类型取出来放在一个数组里。看测试用例就能明白。

/* _____________ Your Code Here _____________ */

type MyParameters<T extends (... args: any[]) => any> = any



/* _____________ Test Cases _____________ */
import { Equal, Expect, ExpectFalse, NotEqual } from '@type-challenges/utils'

const foo = (arg1: string, arg2: number): void => {}
const bar = (arg1: boolean, arg2: {a: 'A'}): void => {}
const baz = (): void => {}

type cases = [
  Expect<Equal<MyParameters<typeof foo>, [string, number]>>,
  Expect<Equal<MyParameters<typeof bar>, [boolean, {a: 'A'}]>>,
  Expect<Equal<MyParameters<typeof baz>, []>>,
]

分析

这道题比较简单没有什么新知识。结题思路:首先判断传入的类型是不是一个函数类型,是的话就把参数的类型拿出来,不是的话就返回never。

type MyParameters<T extends (... args: any[]) => any> = T extends (...args: infer U) => any ? U: never

然后,根据这道题来进行举一反三,实现TS内置工具函数ReturnType,它的作用就是提取出函数type返回的类型。其实基本和上面是一个思路:

type ReturnType<T extend (... args: any[]) => any> = T extends (...args:any[]) => U ? U : never;

做到这里easy的题目就已经做完了,应该开启下一步medium了。

medium

1.Get Return Type

题目:实现 TypeScript 的内置工具 ReturnType<T> 泛型。 例子:

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

type a = MyReturnType<typeof fn> // 应推导出 "1 | 2"
/* _____________ Your Code Here _____________ */
type MyReturnType<T> = any;

/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<string, MyReturnType<() => string>>>,
  Expect<Equal<123, MyReturnType<() => 123>>>,
  Expect<Equal<ComplexObject, MyReturnType<() => ComplexObject>>>,
  Expect<Equal<Promise<boolean>, MyReturnType<() => Promise<boolean>>>>,
  Expect<Equal<() => 'foo', MyReturnType<() => () => 'foo'>>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn>>>,
  Expect<Equal<1 | 2, MyReturnType<typeof fn1>>>,
]

type ComplexObject = {
  a: [12, 'foo']
  bar: 'hello'
  prev(): number
}

const fn = (v: boolean) => v ? 1 : 2
const fn1 = (v: boolean, w: any) => v ? 1 : 2

分析

  1. 首先Return就限制了T必须是函数类型,所以首先要对这个做限制 -> 条件类型
  2. 然后看测试用例,其实函数return出的类型不是确定的,所以这时我们需要想到infer关键字,来做推测。
type MyReturnType<T> = T extends (...args:any[]) => infer K ? K : never;

2.Omit

题目:实现内置的工具泛型Omit<T, K>Omit会返回一个新的接口T,这个新的接口T剔除了属性K。

示例代码:

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

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

const todo: TodoPreview = {
  completed: false,
}

看这个示例是不是觉得有点熟悉,很像之前有写过Exclude工具类型,Exclude实现的功能是从Union类型里剔除掉属性。这里是从一个接口对象里过滤属性,其实思路是差不多。

/* _____________ Your Code Here _____________ */

type MyOmit<T, K> = any


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<Expected1, MyOmit<Todo, 'description'>>>,
  Expect<Equal<Expected2, MyOmit<Todo, 'description' | 'completed'>>>,
]

// @ts-expect-error
type error = MyOmit<Todo, 'description' | 'invalid'>

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

interface Expected1 {
  title: string
  completed: boolean
}

interface Expected2 {
  title: string
}

分析

  1. 首先是想一下用JS的思路,首先新建一个空对象用来保存最终的结果;
  2. 然后我们会去遍历一遍接口T的所有属性;
  3. 最后取出T的每一个key和K的每一个(K是Union)对比,如果不等就存到新对象里。

接下来就按到上述的步骤来做:

  1. 新建空对象
type MyOmit<T, K> = {}
  1. 遍历接口T的所有属性 keyof T可以取到接口T的所有属性组合成为一个Union,比如keyof Todo得到的结果就是title|description|completed。这里涉及到了keyof知识点

  2. 取出T的key和K对比 第三步单拿出来就是一个题目Pick,大家还记得吗,从类型T中选取属性K。

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

只不过这里我们不是选取属性K,我们是过滤掉属性K,使用Exclude来过滤:

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

写到上面这一步,会发现// @ts-expect-error还是报错,发现我们没有限制属性K,补一个K extends keyof T就OK了。

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

3.Readonly 2

题目:再次实现Readonly类型,这次和easy里的Readonly的区别是这次可以传入可选参数,只将传入的参数置为readonly,如果不传默认将所有参数置为readonly

/* _____________ Your Code Here _____________ */

type MyReadonly2<T, K> = any;

/* _____________ Test Cases _____________ */
import type { Alike, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Alike<MyReadonly2<Todo1>, Readonly<Todo1>>>,
  Expect<Alike<MyReadonly2<Todo1, 'title' | 'description'>, Expected>>,
  Expect<Alike<MyReadonly2<Todo2, 'title' | 'description'>, Expected>>,
]

interface Todo1 {
  title: string
  description?: string
  completed: boolean
}

interface Todo2 {
  readonly title: string
  description?: string
  completed: boolean
}

interface Expected {
  readonly title: string
  readonly description?: string
  completed: boolean
}

分析

这道题用JS来解答的话,有两种思路:第一种是用双重for循环一一对比T的key和K的key是不是相等,如果相等就给readonly;第二种就是将K限制为T的子集直接将K的key进行readonly,然后再去取T中把K剔除的属性组合在一起。

第一种是双重for循环,在TS里很少见到会这么去写类型的,所以我们选择第二种做法。

首先是把T里包含K的key进行readonly,这个需要循环一次:

type MyReadonly2<T, K extends keyof T> = {
 readonly [K in keyof T]: T[K]
}

上面就完成了第一步,把T中包含K的key进行readonly保存。此时T中不属于K的那部分属性怎么办呢,想到之前我们才写过的Omit<T,K>表示从T中把K剔除出去,刚好符合现在的场景。这里还涉及一个知识点就是&符号,在TS里表示并集,所以我们取到剔除K的对象和前面已经readonly的对象合并一下就是我们最终需要的结果了。

type MyReadonly2<T, K extends keyof T> = {
 readonly [K in keyof T]: T[K]
} & Omit<T, K>

4.Deep Readonly

题目:实现一个通用的DeepReadonly<T>,它将对象的每个参数及其子对象递归地设为只读。 您可以假设在此挑战中我们仅处理对象。数组,函数,类等都无需考虑。但是,您仍然可以通过覆盖尽可能多的不同案例来挑战自己。

/* _____________ Your Code Here _____________ */

type DeepReadonly<T> = any;


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<DeepReadonly<X>, Expected>>,
]

type X = {
  a: () => 22
  b: string
  c: {
    d: boolean
    e: {
      g: {
        h: {
          i: true
          j: 'string'
        }
        k: 'hello'
      }
      l: [
        'hi',
        {
          m: ['hey']
        },
      ]
    }
  }
}

type Expected = {
  readonly a: () => 22
  readonly b: string
  readonly c: {
    readonly d: boolean
    readonly e: {
      readonly g: {
        readonly h: {
          readonly i: true
          readonly j: 'string'
        }
        readonly k: 'hello'
      }
      readonly l: readonly [
        'hi',
        {
          readonly m: readonly ['hey']
        },
      ]
    }
  }
}

分析

这道题简单来说就是将key加上readonly,只是需要判断value是对象或者数组的时候要递归将后面的继续加readonly。

  1. 首先我们第一步就是将第一层的key加上readonly;
  2. 第二步,根据value值判断之后去递归加readonly。 来看第一步:
type DeepReadonly<T> = {
  readonly [K extends keyof T]: T[K]
}

上面就是我们第一次写easy readonly的写法。

然后第二步,我们需要对T[K]进行判断:

这里取个巧,因为对象和数组都是属于有key(JS里数组也是对象),所以我们通过判断T[K]key是否存在来判断是否是对象和数组

type DeepReadonly<T> = {
  readonly [K extends keyof T]: keyof T[K] extends never ? T[K]: DeepReadonly<T[K]>
}

因为基础数据类型是没有key的,所以会extends never

5.Tuple to Union

题目:实现泛型TupleToUnion<T>,它返回元组所有值的合集。

/* _____________ Your Code Here _____________ */

type TupleToUnion<T> = any


/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<TupleToUnion<[123, '456', true]>, 123 | '456' | true>>,
  Expect<Equal<TupleToUnion<[123]>, 123>>,
]

分析 这道题其实还比较简单,所以实现方式真的是五花八门了。这里讲一种最简单的方式:

首先Tuple表示数组里的元素是固定的,我们要把元素拿出来组成一个元组,而拿出数组的元素就非常简单粗暴了直接T[number],刚好元素也会自动组成元组。

type TupleToUnion<T extends any[]> = T[number];

6.Chainable Options