测试题目
3333字约11分钟
TypeScript
2023-01-30
Pick
不使用 Pick<T, K>
,实现 TS 内置的 Pick<T, K>
的功能。
从类型 T 中选出符合 K 的属性,构造一个新的类型。
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
解答
type MyPick<T, K extends keyof T> = {
[key in K]: T[key];
}
Readonly
不要使用内置的Readonly<T>
,自己实现一个。
泛型 Readonly<T>
会接收一个 泛型参数,并返回一个完全一样的类型,只是所有属性都会是只读 (readonly)
的。
也就是不可以再对该对象的属性赋值。
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
解答
type MyReadonly<T> = {
readonly [key in keyof T]: T[key]
}
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'}
解答
type TupleToObject<T extends readonly any[]> = {
[key in T[number]]: key;
}
First of Array
实现一个First<T>
泛型,它接受一个数组T并返回它的第一个元素的类型。
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // 应推导出 'a'
type head2 = First<arr2> // 应推导出 3
解答
//answer1
type First<T extends any[]> = T extends [] ? never : T[0]
//answer2
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
//answer3
type First<T extends any[]> = T extends [infer A, ...infer rest] ? A : never
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
解答
type Lenght<T extends readonly any[]> = T['length']
Exclude
实现内置的 Exclude<T, U>
类型,但不能直接使用它本身。
从联合类型 T 中排除 U 中的类型,来构造一个新的类型。
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
解答
type MyExclude<T, U> = T extends U ? never : T;
Awaited
假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。
例如:Promise<ExampleType>
,请你返回 ExampleType 类型。
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
解答
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U>
? U extends PromiseLike<any>
? MyAwaited<U>
: U
: never;
定义了一个类型别名MyAwaited,它用于递归地解析Promise类型,直到内部值不再是一个Promise。具体来说:
T extends PromiseLike<any>
:首先,它检查参数类型T是否类似于Promise,如果是,则进行下一步;否则,返回never类型。U extends PromiseLike<any>
:使用infer U从T中推断出一个类型U,然后检查U是否类似于Promise,如果是,则递归调用MyAwaited<U>
;否则,返回U类型。- 这个类型的目的是为了在类型系统中模拟await操作,将Promise链中的每个Promise解析到最后的非Promise值。
If
实现一个 IF 类型,它接收一个条件类型 C ,一个判断为真时的返回类型 T ,以及一个判断为假时的返回类型 F。 C 只能是 true 或者 false, T 和 F 可以是任意类型。
type A = If<true, 'a', 'b'> // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'
解答
type If<C extends boolean, T, F> = C extends true ? T : F;
Concat
在类型系统里实现 JavaScript 内置的 Array.concat
方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。
type Result = Concat<[1], [2]> // expected to be [1, 2]
解答
type Tuple = readonly unknown[];
// answer1
type Concat<T extends Tuple, U extends Tuple> = [...T, ...U];
Includes
在类型系统里实现 JavaScript 的 Array.includes
方法,这个类型接受两个参数,返回的类型要么是 true 要么是 false。
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
解答
type Includes<T extends readonly any[], U> = {
[P in T[number]]: true
}[U] extends true ? true : false;
Push
在类型系统里实现通用的 Array.push
。
type Result = Push<[1, 2], '3'> // [1, 2, '3']
解答
type Push<T extends unknown[], U> = [...T, U]
Unshift
实现类型版本的 Array.unshift
。
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
解答
type Push<T extends unknown[], U> = [U, ...T]
Parameters
实现内置的 Parameters
类型,而不是直接使用它,可参考TypeScript官方文档。
const foo = (arg1: string, arg2: number): void => {}
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
解答
type MyParameters<T extends Function> = T extends (...args: infer Item) => unknown ? [...Item]: never;
Get Return Type
不使用 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 extends (...args: never[]) => unknown> =
T extends (...args: never[]) => infer R
? R
: never
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, U extends keyof T> = { [key in keyof T as key extends U ? never : key]: T[key] }
Readonly 2
实现一个泛型MyReadonly2<T, K>
,它带有两种类型的参数T和K。
类型 K 指定 T 中要被设置为只读 (readonly)
的属性。如果未提供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
解答
// answer1
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> & Readonly<Pick<T, K>>;
// answer2
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly [key in keyof T as key extends K ? key : never]: T[key];
} & {
[key in keyof T as key extends K ? never : key]: T[key];
}
Deep 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> = keyof T extends never
? T
: { readonly [k in keyof T]: DeepReadonly<T[k]> };
keyof T extends never
: 判断 T 是否没有任何属性,如果是,则直接返回 T
Tuple to Union
实现泛型TupleToUnion<T>
,它返回元组所有值的合集。
type Arr = ['1', '2', '3']
type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'
解答
// answer1
export type TupleToUnion<T> = T extends Array<infer ITEMS> ? ITEMS : never
// answer2
type TupleToUnion<T extends unknown[]> = T[number];
Chainable Options
在 JavaScript 中我们经常会使用可串联(Chainable/Pipeline)的函数构造一个对象,但在 TypeScript 中,你能合理的给它赋上类型吗?
在这个挑战中,你可以使用任意你喜欢的方式实现这个类型 - Interface, Type 或 Class 都行。你需要提供两个函数 option(key, value)
和 get()
。在 option 中你需要使用提供的 key
和 value
扩展当前的对象类型,通过 get
获取最终结果。 例如:
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
}
}
你只需要在类型层面实现这个功能 - 不需要实现任何 TS/JS 的实际逻辑。
你可以假设 key 只接受字符串而 value 接受任何类型,你只需要暴露它传递的类型而不需要进行任何处理。同样的 key 只会被使用一次。
解答
type Chainable<T = {}> = {
option: <K extends string, V>(key: K extends keyof T ?
V extends T[K] ? never : K
: K, value: V) => Chainable<Omit<T, K> & Record<K, V>>
get: () => T
}
前三步基于:
可以使用 T = {} 来作为默认值,甚至默认参数与默认返回值,再通过递归传递 T 即可实现递归全局记录 option 是一个函数接收两个值:K 和 V,为了约束 key 不可重复必须范型传入,value 是任意类型范型不做约束直接透传
type Chainable<T = {}> = {
option: <K extends string, V>(key: K, value: V) => Chainable<T & Record<K, V>>
get: () => T
}
先验证重复 key,实现传入相同 key 报错
type Chainable<T = {}> = {
option: <K extends string, V>(key: K extends keyof T ? never : K, value: V)
=> Chainable<T & Record<K, V>>
get: () => T
}
4.然后发现案例3无法通过,案例3是传入了相同的 key 但类型不同,因此在 K extends keyof T 后面增加验证只有类型相同才返回 never
type Chainable<T = {}> = {
option: <K extends string, V>(key: K extends keyof T ?
V extends T[K] ? never : K
: K, value: V) => Chainable<T & Record<K, V>>
get: () => T
}
5.最后直接 & 联合并不能将相同 key 的类型覆盖,因此用 Omit 去掉前一个类型中相同的 key
type Chainable<T = {}> = {
option: <K extends string, V>(key: K extends keyof T ?
V extends T[K] ? never : K
: K, value: V) => Chainable<Omit<T, K> & Record<K, V>>
get: () => T
}
Last of Array
在此挑战中建议使用TypeScript 4.0
实现一个Last<T>
泛型,它接受一个数组T并返回其最后一个元素的类型。
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type tail1 = Last<arr1> // 应推导出 'c'
type tail2 = Last<arr2> // 应推导出 1
解答
// answer1
type Last<T extends any[]> = T extends [...infer rest, infer last] ? last : never
// answer2
type Last<T extends any[]> = T extends []? never: [0, ...T][T['length']]
// answer3
type Last<T extends any[]> = [any, ...T][T["length"]];
Pop
实现一个泛型Pop<T>
,它接受一个数组T,并返回一个由数组T的前 N-1 项(N 为数组T的长度)以相同的顺序组成的数组。
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]
解答
type Pop<T extends any[]> = T extends [...infer rest, any]? rest: never;
Promise.all
给函数PromiseAll
指定类型,它接受元素为 Promise
或者类似 Promise
的对象的数组,返回值应为Promise<T>
,其中T
是这些 Promise
的结果组成的数组。
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise<string>((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
// 应推导出 `Promise<[number, 42, string]>`
const p = PromiseAll([promise1, promise2, promise3] as const)
解答
declare function PromiseAll<T extends any[]>(args: readonly [...T]): Promise<{
[key in keyof T]: Awaited<T[key]>
}>;
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`
解答
// answer1
type LookUp<T, K extends string> = T extends { type: K } ? T : never;
// answer2
type LookUp<T extends { type: string }, U extends string> = T extends unknown
? T['type'] extends U
? T
: never
: never
Trim Left
实现 TrimLeft<T>
,它接收确定的字符串类型并返回一个新的字符串,其中新返回的字符串删除了原字符串开头的空白字符串。
type trimed = TrimLeft<' Hello World '> // expected to be 'Hello World '
解答
type Space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${Space}${infer R}` ? TrimLeft<R> : S
Trim
实现Trim<T>
,它接受一个明确的字符串类型,并返回一个新字符串,其中两端的空白符都已被删除。
type trimed = Trim<' Hello World '> // expected to be 'Hello World'
解答
type Space = ' ' | '\t' | '\n';
type Trim<S extends string> = S extends `${Space}${infer T}` | `${infer T}${Space}` ? Trim<T> : S;
Capitalize
实现 Capitalize<T>
它将字符串的第一个字母转换为大写,其余字母保持原样。
type capitalized = Capitalize<'hello world'> // expected to be 'Hello world'
解答
type Capitalize<S extends string> = S extends `${infer x}${infer tail}` ? `${Uppercase<x>}${tail}` : S;
Replace
实现 Replace<S, From, To>
将字符串 S 中的第一个子字符串 From 替换为 To 。
type replaced = Replace<'types are fun!', 'fun', 'awesome'> // 期望是 'types are awesome!'
解答
type Replace<S extends string, From extends string, To extends string> =
From extends ''
? S
: S extends `${infer V}${From}${infer R}`
? `${V}${To}${R}`
: S
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> = From extends ''
? S
: S extends `${infer R1}${From}${infer R2}`
? `${R1}${To}${ReplaceAll<R2, From, To>}`
: S
Append Argument
实现一个泛型 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
解答
type AppendArgument<Fn extends (...args: any) => any, A> = Fn extends (...args: infer T) => infer R ? (...args: [...T, x: A]) => R : never;
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']
解答