🌞Moon Will Know
⚔️

TS 类型挑战

这里主要记录一些在 type-challenges 中遇到的有意思的挑战,以及一些不熟悉的 TS 语法。

18. Length of Tuple

For given a tuple, you need create a generic Length, pick the length of the tuple.
For example:
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
answer:
type Length<T extends readonly[]> = T['length'];

知识点

  1. 对于 readonly [] 或元组类型来说,length 属性是一个数字字面量。
  1. 如果元组中包含可选属性,或使用了剩余元素语法,length 属性则只会是 number
  1. 元组是特殊的数组。

898.Includes

Implement the JavaScript Array.includes function in the type system. A type takes the two arguments. The output should be a boolean true or false.
For example:
export type Equals<X, Y> = (<T>() => T extends X ? "A" : "B") extends (<T>() => T extends Y ? "A" : "B") ? true : false; type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
answer:
type Includes<T,U> = T extends [infer F, ...infer rest] ? Equal<F,U> extends true ? true : Includes<rest,U> : fasle

知识点

  1. infer 可以在正在比较的的类型中推断类型,一般同 extends 一起使用。
  1. 对于 Array 类型,可以使用解构语法解构。
  1. 如果需要使用遍历的时候,可以考虑递归。

3.Omit

Implement the built-in Omit<T, K> generic without using it. Constructs a type by picking all properties from T and then removing K
For example
interface Todo { title: string description: string completed: boolean } type TodoPreview = MyOmit<Todo, 'description' | 'title'> const todo: TodoPreview = { completed: false, }
answer:
type MyOmit<T extends {}, K extends keyof T> = { [Key in keyof T as Key extends K ? never : Key] : T[Key] }

知识点

  1. 通过 as 实现键名重新映射(Key Remapping via as )
    1. 一般用来用已有的属性名通过模版字面量拓展成新的属性名。
    2. 或者是通过条件判断返回 never 来过滤属性。
    3. 或者是在遍历非字符串字面量联合类型时用索引访问器取出任意值。

8. Readonly 2

Implement a generic MyReadonly2<T, K> which takes two type argument T and K.
K specify the set of properties of T that should set to Readonly. When K is not provided, it should make all properties readonly just like the normal Readonly<T>.
For example:
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
answer:
type MyReadonly2<T,U = keyof T> = { readonly [K in keyof T as K extends U ? K :never]: T[K] } & { [K in Excart<keyof T, U>]: T[K] }

知识点

  1. 当需要重复遍历才能解决时,考虑交集类型。
  1. 使用 = 可以给泛型参数设置默认值。

9. Deep Readonly

Implement a generic DeepReadonly<T> which make every parameter of an object - and its sub-objects recursively - readonly.
You can assume that we are only dealing with Objects in this challenge. Arrays, Functions, Classes and so on do not need to be taken into consideration. However, you can still challenge yourself by covering as many different cases as possible.
For example:
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`
answer:
type DeepReadonly<T> = { [K in T]: keyof T[K] extends never ? T[K] : DeepReadonly<T[K]> }

知识点

  1. 深度遍历时,判断是否已经到最底层的依据是 keyof T[K] extends never

10. Tuple to Union

Implement a generic TupleToUnion<T> which covers the values of a tuple to its values union.
For example:
type Arr = ['1', '2', '3'] type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'
answer:
type TupleToUnion<T extends any[]> = T[number];

知识点

  1. 数组是使用索引签名定义的对象,所以通过索引访问 number 就能获取所有值的类型,而元组的成员的数量固定且成员类型为字符串字面量。(猪脑子啊一直记不住)

12.Chainable Options

Chainable options are commonly used in Javascript. But when we switch to TypeScript, can you properly type it?
In this challenge, you need to type an object or a class - whatever you like - to provide two function option(key, value) and get(). In option, you can extend the current config type by the given key and value. We should about to access the final result via get.
For example:
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 } }
answer:
type Chainable<T = {}> = { option<K extends string, V extends any> (key: K extends keyof T ? (V extends T[K] ? never : K) : K , value: V) : Chainable<{ [Key in K]: V } & Omit<T, K>> get(): T }

知识点

  1. 使用 泛型 T 来保存了当前对象的类型,设置了默认值,并在接下来的重复调用中替换为上一次调用的结果的类型。
  1. 题目要求在 key相同时,当类型改变时才能改变对象的值,所以第3 - 5 行判断了T中已有的值和 value的类型来收束了key的类型。
  1. 由于 K 已经是被收束后的类型,所以使用 Omit 工具类型来排除 T 中重复的类型。
  1. [Key in K] 在索引签名中使用映射类型可以将 字符串字面量联合类型设置为类型的属性。

参考

20. Promise.all

Type the function PromiseAll that accepts an array of PromiseLike objects, the returning value should be Promise<T> where T is the resolved result array.
for example:
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)
answer:
declare function PromiseAll<T extends any[]>(values: readonly [...T]) : Promise<{ [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K] }>

知识点

  1. 定义泛型参数为 readonly 数组, readonly [...T]
  1. 数组类型是特殊的对象类型。

106. Trim Left

Implement TrimLeft<T> which takes an exact string type and returns a new string with the whitespace beginning removed.
For example:
type trimed = TrimLeft<' Hello World '> // expected to be 'Hello World '
answer:
type TrimLeft<T extends string> = T extends ` ${infer S}` ? TrimLeft<S> : T extends `\\n\\t${infer S}` ? TrimLeft<S> : T //or type Spaces = ' ' | '\\n' | '\\t' type TrimLeft<T extends string> = T extends `${Spaces}${infer S}` ? TrimLeft<S> : T

知识点

  1. 字面量模版类型中如果存在一个变量是联合类型,那么则会采用分配律。
  1. 使用 infer 可以从正在进行条件判断的类型中推断出类型。

110.Capitalize

Implement Capitalize<T> which converts the first letter of a string to uppercase and leave the rest as-is.
For example
type capitalized = Capitalize<'hello world'> // expected to be 'Hello world'
answer
type Capitalize<S extends string> = S extends `${infer F}${infer Rest}` ? `${Uppercase<F>}${Rest}` : S

知识点

  1. 对字面量使用 infer , 第一个被推断出来的将是第一个字母组成的字符串字面量类型或是我们的泛型变量的前面一部分,最后一个推断类型是剩余的字符串字面量类型。
  1. Uppercase 工具类型将所有的字母转换为大写。

191.Append Argument

For given function type Fn, and any type A (any in this context means we don't restrict the type, and I don't have in mind any type 😉) create a generic type which will take Fn as the first argument, A as the second, and will produce function type G which will be the same as Fn but with appended argument A as a last one.
For example,
type Fn = (a: number, b: string) => number type Result = AppendArgument<Fn, boolean> // expected be (a: number, b: string, x: boolean) => number
answer
type AppendArgument<Fn extends (...args: any[])=> unknown,A> = Fn extends (...args: infer P)=> infer R ? (...args: [...P,A])=> R : never

知识点

  1. 定义函数参数和返回值为任意:(...args: any[])=>unknown
  1. 使用 infer 推断函数的参数类型和返回值类型:Fn extends (...args: infer P)=> infer R