这里主要记录一些在 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'];
知识点
- 对于
readonly []
或元组类型来说,length
属性是一个数字字面量。
- 如果元组中包含可选属性,或使用了剩余元素语法,
length
属性则只会是number
。
- 元组是特殊的数组。
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
知识点
infer
可以在正在比较的的类型中推断类型,一般同extends
一起使用。
- 对于
Array
类型,可以使用解构语法解构。
- 如果需要使用遍历的时候,可以考虑递归。
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] }
知识点
- 通过 as 实现键名重新映射(Key Remapping via as )
- 一般用来用已有的属性名通过模版字面量拓展成新的属性名。
- 或者是通过条件判断返回
never
来过滤属性。 - 或者是在遍历非字符串字面量联合类型时用索引访问器取出任意值。
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] }
知识点
- 当需要重复遍历才能解决时,考虑交集类型。
- 使用
=
可以给泛型参数设置默认值。
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]> }
知识点
- 深度遍历时,判断是否已经到最底层的依据是
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];
知识点
- 数组是使用索引签名定义的对象,所以通过索引访问
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 }
知识点
- 使用 泛型
T
来保存了当前对象的类型,设置了默认值,并在接下来的重复调用中替换为上一次调用的结果的类型。
- 题目要求在
key
相同时,当类型改变时才能改变对象的值,所以第3 - 5 行判断了T
中已有的值和value
的类型来收束了key
的类型。
- 由于
K
已经是被收束后的类型,所以使用Omit
工具类型来排除T
中重复的类型。
[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] }>
知识点
- 定义泛型参数为
readonly
数组,readonly [...T]
。
- 数组类型是特殊的对象类型。
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
知识点
- 字面量模版类型中如果存在一个变量是联合类型,那么则会采用分配律。
- 使用
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
知识点
- 对字面量使用
infer
, 第一个被推断出来的将是第一个字母组成的字符串字面量类型或是我们的泛型变量的前面一部分,最后一个推断类型是剩余的字符串字面量类型。
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
知识点
- 定义函数参数和返回值为任意:
(...args: any[])=>unknown
- 使用
infer
推断函数的参数类型和返回值类型:Fn extends (...args: infer P)=> infer R