type-challenges 是一个typescript类型体操的解题开源项目,可以让你像刷leetcode使用typescript来实现一些功能。我们所熟知的typescript只是用来编写js的时候作为类型定义,其实typescript能做的事情远不止这些,它也可以同js一样,实现一些逻辑运算,而有些使用typescript实现功能的操作比较难以理解说,所以也被戏称为"类型体操"。
在type-challenges 中一共有4种难度的题目,分别是:easy(简单)、medium(中等)、hard(难)、extreme(地狱)。
本篇文章会先实现easy类型的题目。
题目一:实现Pick
题目:
不使用 Pick<T, K> ,实现 TS 内置的 Pick<T, K> 的功能。
从类型 T 中选出符合 K 的属性,构造一个新的类型。
例如:
ts
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
解答:
其实这道题是模拟ts的内置工具类型:Pick。作用是从一个类型中挑出某些属性。
ts
type MyPick<T, K extends keyof T> = {
[k in K]: T[k]
}
keyof 是 ts 中的一种类型操作符,用于获取一个类型的所有键(属性名)作为联合类型。它的语法如下:
ts
type Keys = keyof ObjType
其中:
- keyof 是 TypeScript 的关键字,用于指定要获取键的类型操作。
- ObjType 是想要获取键的对象类型。
举个🌰:
ts
type ObjType = { name: string; age: number };
type Keys = keyof ObjType;
// Keys = "name" | "age"
首先第二个泛型K被约束为T类型所有键(属性名)的联合类型,也就是K类型必须为T类型的子集。
ts
[k in K]: T[k]
in 语法在ts中作为遍历操作,挨个取出K类型的所有键,然后通过键获取对应在T类型中的值。所以MyPick用js的语法来看,类似于以下逻辑:
ts
const obj = {
name: '瓜',
age: 18,
phone: 123
}
const pickAry = ['name', 'age']
let newObj = {}
function myPick(obj, pickAry) {
for(let i = 0; i < pickAry.length; i++) {
let key = pickAry[i]
newObj[key] = obj[key]
}
}
题目二:对象属性只读
题目:
不要使用内置的Readonly,自己实现一个。
泛型 Readonly 会接收一个 泛型参数,并返回一个完全一样的类型,只是所有属性都会是只读 (readonly) 的。
也就是不可以再对该对象的属性赋值。
例如:
ts
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
解答:
ts
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
同样是利用 in 操作符,将所有的对象中键进行遍历,然后通过 T 类型取值,只不过在前面加上readonly。
题目三:元组转换为对象
题目:
将一个元组类型转换为对象类型,这个对象类型的键/值和元组中的元素对应。
例如:
ts
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'}
解答:
ts
type TupleToObject<T extends readonly PropertyKey[]> = {
[K in T[number]]: K
}
T 类型被约束为只读类型,直接用ts内置的类型PropertyKey,类型定义是:
ts
type PropertyKey = string | number | symbol;
其实如果想限制基本类型,可以使用:
ts
type Test = keyof any
// Test = string | number | symbol
keyof操作符是用于提取后面对象键的操作,那么keyof any,意思就是提取一个any类型的对象键,但是对象键只可能是 string number symbol。
题目四:第一个元素
题目:
实现一个First泛型,它接受一个数组T并返回它的第一个元素的类型。
例如:
ts
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // 应推导出 'a'
type head2 = First<arr2> // 应推导出 3
解答:
infer关键字可以获取指定的元素通过位置获取不同的值。
ts
type First<T extends any[]> = T extends [infer P, ...any[]] ? P : never
获取最后一项,同时也可以通过 R 获取剩余项。
ts
type First<T extends any[]> = T extends [...infer R, infer P] ? P : never
题目五:获取元组长度
题目:
创建一个Length泛型,这个泛型接受一个只读的元组,返回这个元组的长度。
例如:
ts
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
解答:
ts 中同样可以通过length获取长度。
ts
type Length<T extends readonly any[]> = T['length']
题目六:实现 Exclude
题目:
实现内置的 Exclude<T, U> 类型,但不能直接使用它本身。
从联合类型 T 中排除 U 中的类型,来构造一个新的类型。
例如:
ts
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
解答:
T 类型的每一项都会与U类型比较,never会被忽略
r
type MyExclude<T, U> = T extends U ? never : T;
ts
type A = MyExclude<'key1' | 'key2', 'key2'>
// 等价于
type A = MyExclude<'key1', 'key2'> | MyExclude<'key2', 'key2'>
// =>
type A = ('key1' extends 'key2' ? never : 'key1') | ('key2' extends 'key2' ? never : 'key2')
// =>
// never是所有类型的子类型
type A = 'key1' | never = 'key1'
题目七:Awaited
题目:
假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。
例如:Promise,请你返回 ExampleType 类型。
ts
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
解答:
具有 then 方法的对象就是 PromiseLike。
ts
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U>
? U extends PromiseLike<any>
? MyAwaited<U>
: U
: never;
题目八:if
题目:
实现一个 IF 类型,它接收一个条件类型 C ,一个判断为真时的返回类型 T ,以及一个判断为假时的返回类型 F。 C 只能是 true 或者 false, T 和 F 可以是任意类型。
例如:
ts
type A = If<true, 'a', 'b'> // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'
解答:
ts
type If<C extends boolean, T, F> = C extends true ? T : F;
题目九:Concat
题目:
在类型系统里实现 JavaScript 内置的 Array.concat 方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。
例如:
ts
type Result = Concat<[1], [2]> // expected to be [1, 2]
解答:
ts
type Tuple = readonly unknown[];
type Concat<T extends Tuple, U extends Tuple> = [...T, ...U];
题目十:Includes
题目:
在类型系统里实现 JavaScript 的 Array.includes 方法,这个类型接受两个参数,返回的类型要么是 true 要么是 false。
例如:
ts
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
解答:
在ts中同样可以实现递归的逻辑,通过调用自己,每次调用时取出其中一位与U进行比较。
ts
type Includes<T extends readonly any[], U> = T extends [infer L, ...infer R]
? [U, L] extends [L, U]
? true
: Includes<R, U>
: false
题目十一:push
题目:
在类型系统里实现通用的 Array.push 。
例如:
ts
type Result = Push<[1, 2], '3'> // [1, 2, '3']
解答:
ts
type Push<T extends unknown[], U> = [...T, U]
题目十二:Unshift
题目:
实现类型版本的 Array.unshift。
例如:
ts
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
解答:
ts
type Unshift<T extends unknown[], U> = [U, ...T]
题目十三:Parameters
题目:
实现内置的 Parameters 类型,而不是直接使用它,可参考TypeScript官方文档。
例如:
ts
const foo = (arg1: string, arg2: number): void => {}
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
解答:
infer 同样可以获取函数的参数类型:
ts
type MyParameters<T extends (...args: any[]) => any> = T extends (...any: infer S) => any ? S : any
写在最后 ⛳
未来可能会更新实现mini-react
和antd
源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳