题单(中等)
解答
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>
,它带有两种类型的参数 T
和 K
。
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]
额外 :同样,您也可以实现 Shift
,Push
和 Unshift
吗?
正确解答 :看了小册后不难做出
复制 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'>
获得 Dog
,LookUp<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
,返回一个新的函数 G
。G
拥有 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
}