Middle(20/72)

Middle题共72道,是类型体操的大头

题单(中等)

解答

2・获取函数返回类型

不使用 ReturnType 实现 TypeScript 的 ReturnType<T> 泛型。

例如:

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

type a = MyReturnType<typeof fn> // 应推导出 "1 | 2"

正确解答:

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

注意参数数组~

3・实现 Omit

不使用 Omit 实现 TypeScript 的 Omit<T, K> 泛型。

Omit 会创建一个省略 K 中字段的 T 对象。

例如:

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> = {
  [Key in keyof T as Key extends K ? never : Key]: T[Key]
}

乍一看很绕,其实搞懂 keyof 和 in 就明白了。

8・Readonly 2

实现一个通用 MyReadonly2<T, K>,它带有两种类型的参数 TK

K指定应设置为 Readonly 的 T的属性集。如果未提供 K,则应使所有属性都变为只读,就像普通的 Readonly<T>一样。

例如

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

正确解答

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

结合上面的 实现 Omit 和简单题中的 实现 Readonly 可得出该题的答案。

9・深度 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`

正确解答

type DeepReadonly<T extends Object> = {
  readonly [Key in keyof T]: T[Key] extends Record<any, any>
    ? T[Key] extends Function
      ? T[Key]
      : DeepReadonly<T[Key]>
    : T[Key]
}

递归解决,注意特判 Funtion 的情况

10・元组转合集

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

例如

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

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

解答: 没想太多的递归实现 👇

type TupleToUnion<T> = T extends [infer V, ...infer Rest] ? V | TupleToUnion<Rest> : never

翻了翻 issue 发现的另一个实现,确实哦!

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

12・可串联构造器 ⭐⭐

在 JavaScript 中我们经常会使用可串联(Chainable/Pipeline)的函数构造一个对象,但在 TypeScript 中,你能合理的给它赋上类型吗?

在这个挑战中,你可以使用任意你喜欢的方式实现这个类型 - Interface, Type 或 Class 都行。你需要提供两个函数 option(key, value)get()。在 option 中你需要使用提供的 key 和 value 扩展当前的对象类型,通过 get 获取最终结果。

例如

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

declare const config: Chainable

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

// 期望 result 的类型是:
interface Result {
  foo: number
  name: string
  bar: {
    value: string
  }
}

你只需要在类型层面实现这个功能 - 不需要实现任何 TS/JS 的实际逻辑。

你可以假设 key 只接受字符串而 value 接受任何类型,你只需要暴露它传递的类型而不需要进行任何处理。同样的 key 只会被使用一次。

正确解答:注意 Key 必须为之前没取过的,或是 Value 类型与之前不同的(用例 2 和 3)。

type Chainable<Obj extends Record<string, any> = {}> = {
  option<K extends string, V>(
    key: K extends keyof Obj ? (V extends Obj[K] ? never : V) : K,
    value: V,
  ): Chainable<Omit<Obj, K> & Record<K, V>>
  get(): Obj
}

15・最后一个元素

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

正确解答:基本送分题

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

16・出堆

实现一个通用 Pop<T>,它接受一个数组 T,并返回一个由数组 T的前 length-1 项以相同的顺序组成的数组。

例如

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]

额外:同样,您也可以实现 ShiftPushUnshift吗?

正确解答:看了小册后不难做出

type Pop<T extends any[]> = T extends [...infer Rest, infer L] ? Rest : T
type Push<T extends any[], Arr extends any[]> = [...T, ...Arr]
type Shift<T extends any[]> = T extends [infer F, ...infer Rest] ? Rest : T
type Unshift<T extends any[], Arr extends any[]> = [...Arr, ...T]

20・Promise.all ⭐⭐⭐

键入函数 PromiseAll,它接受 PromiseLike 对象数组,返回值应为 Promise<T>,其中 T是解析的结果数组。

const promise1 = Promise.resolve(3)
const promise2 = 42
const promise3 = new Promise<string>((resolve, reject) => {
  setTimeout(resolve, 100, 'foo')
})

// expected to be `Promise<[number, 42, string]>`
const p = PromiseAll([promise1, promise2, promise3] as const)

正确解答

declare function PromiseAll<Args extends readonly unknown[]>(
  values: readonly [...Args],
): Promise<{ [Key in keyof Args]: Args[Key] extends Promise<infer V> ? V : Args[Key] }>

疑惑吗?疑惑就对了,请看看这个 pr :Variadic tuple types

62・Type Lookup ⭐⭐

有时,您可能希望根据某个属性在联合类型中查找类型。

在此挑战中,我们想通过在联合类型 Cat | Dog中搜索公共 type字段来获取相应的类型。换句话说,在以下示例中,我们期望 LookUp<Dog | Cat, 'dog'>获得 DogLookUp<Dog | Cat, 'cat'>获得 Cat

interface Cat {
  type: 'cat'
  breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal'
}

interface Dog {
  type: 'dog'
  breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer'
  color: 'brown' | 'white' | 'black'
}

type MyDog = LookUp<Cat | Dog, 'dog'> // expected to be `Dog`

正确解答:第一反应如下

type LookUp<U, T extends string> = U extends { type: infer S } & Record<any, any> ? (S extends T ? U : never) : never

翻了翻 issue,很巧妙地一个解答如下:

type LookUp<U, T extends string> = U extends { type: T } ? U : never

106・Trim Left

实现 TrimLeft<T> ,它接收确定的字符串类型并返回一个新的字符串,其中新返回的字符串删除了原字符串开头的空白字符串。

例如

type trimed = TrimLeft<'  Hello World  '> // 应推导出 'Hello World  '

正确解答:非常经典的模式提取做匹配

type Space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${Space}${infer NewS}` ? TrimLeft<NewS> : S

108・Trim ⭐

实现Trim<T>,它是一个字符串类型,并返回一个新字符串,其中两端的空白符都已被删除。

例如

type trimed = Trim<'  Hello World  '> // expected to be 'Hello World'

正确解答:结合上一题,易得 TrimRight 的实现方法,二者结合即可实现 Trim

type Space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${Space}${infer NewS}` ? TrimLeft<NewS> : S
type TrimRight<S extends string> = S extends `${infer NewS}${Space}` ? TrimLeft<NewS> : S
type Trim<S extends string> = TrimLeft<TrimRight<S>>

也可以用第二种解法

type Space = ' ' | '\n' | '\t'
type Trim<S extends string> = S extends `${Space}${infer NewS}` | `${infer NewS}${Space}` ? Trim<NewS> : S

110・Capitalize

实现 Capitalize<T> 它将字符串的第一个字母转换为大写,其余字母保持原样。

例如

type capitalized = Capitalize<'hello world'> // expected to be 'Hello world'

正确解答:还是通过模式匹配,使用内置类型 Uppercase 进行大写转换

type MyCapitalize<S extends string> = S extends `${infer F}${infer Rest}` ? `${Uppercase<F>}${Rest}` : S

116・Replace ⭐

实现 Replace<S, From, To> 将字符串 S 中的第一个子字符串 From 替换为 To

例如

type replaced = Replace<'types are fun!', 'fun', 'awesome'> // 期望是 'types are awesome!'

正确解答:注意 From 为空的情况

type Replace<S extends string, From extends string, To extends string> = S extends `${infer Pre}${From}${infer Post}`
  ? From extends ''
    ? S
    : `${Pre}${To}${Post}`
  : S

119・ReplaceAll ⭐⭐

实现 ReplaceAll<S, From, To> 将一个字符串 S 中的所有子字符串 From 替换为 To

例如

type replaced = ReplaceAll<'t y p e s', ' ', ''> // 期望是 'types'

正确解答:在上题基础上,递归!

type ReplaceAll<S extends string, From extends string, To extends string> = S extends `${infer Pre}${From}${infer Post}`
  ? From extends ''
    ? S
    : `${Pre}${To}${ReplaceAll<Post, From, To>}`
  : S

191・追加参数

实现一个泛型 AppendArgument<Fn, A>,对于给定的函数类型 Fn,以及一个任意类型 A,返回一个新的函数 GG 拥有 Fn 的所有参数并在末尾追加类型为 A 的参数。

type Fn = (a: number, b: string) => number

type Result = AppendArgument<Fn, boolean>
// 期望是 (a: number, b: string, x: boolean) => number

正确解答:显而易见,本题 easy

type AppendArgument<Fn extends (...args: any) => any, A> = Fn extends (...args: infer Args) => infer R
  ? (...args: [...Args, A]) => R
  : never

296・Permutation ⭐⭐⭐⭐

实现联合类型的全排列,将联合类型转换成所有可能的全排列数组的联合类型。

type perm = Permutation<'A' | 'B' | 'C'> // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']

正确解答:题目字越少,难度越大(x)

本题实现全排列,同时要利用联合类型的分散传参这个特性 Distributive Conditional Types

翻 issue 的时候翻到了这个优秀的解答:TS 类型体操笔记 - 296 Permutation

[T] extends [never] 是由于 never 无法被准确判断,任何extends never的条件语句都会返回never,而我们需要返回的是空数组,在小册中的 套路六:特殊特性要记清 有提到这一点。

type Permutation<T, Rest = T> = [T] extends [never] ? [] : Rest extends Rest ? [T, ...Permutation<Exclude<T, Rest>>] : []

298・Length of String ⭐

计算字符串的长度,类似于 String#length

正确解答:不讲武德版当然就是 type LengthOfString<S extends string> = S['length'] 当然,讲武德版就是这样 ⬇️

type LengthOfString<S extends string, T extends string[] = []> = S extends `${string}${infer Rest}`
  ? LengthOfString<Rest, [string, ...T]>
  : T['length']

459・Flatten ⭐⭐⭐

在这个挑战中,你需要写一个接受数组的类型,并且返回扁平化的数组类型。

例如:

type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, 5]

正确解答:经典的数组拍平,需要注意的是...展开需在外层,Item extends any[] ? ...Flatten<Item> : Item 是不行的......

type Flatten<Arr extends any[]> = Arr extends [infer Item, ...infer Rest]
  ? [...(Item extends any[] ? Flatten<Item> : [Item]), ...Flatten<Rest>]
  : []

527・Append to object ⭐

  • 实现一个为接口添加一个新字段的类型。该类型接收三个参数,返回带有新字段的接口类型。

例如:

type Test = { id: '1' }
type Result = AppendToObject<Test, 'value', 4> // expected to be { id: '1', value: 4 }

正确解答:先将交叉类型整出来,再把交叉类型遍历一遍。

type AppendToObject<T extends Record<string, any>, U extends string, V> = {
  [Key in keyof T]: T[Key]
} & {
  [key in U]: V
} extends infer Obj
  ? { [K in keyof Obj]: Obj[K] }
  : never

看到一种很有趣的 解法

type AppendToObject<T, U extends keyof any, V> = {
  [K in keyof T | U]: K extends keyof T ? T[K] : V
}

最后更新于