在读完 TypeScript 全面进阶指南 小册后,筹备尝试写一个解读 type-challenges 的系列文章,对所学的内容进行坚固,也心愿能对 typescript
有更宽敞的理解。
该系列文章在大的逻辑上会依据题集的艰难水平进行划分,简略局部波及到的基础知识比拟少,分为一篇文章解析,中等、艰难、天堂系列的题目会依据具体波及的常识篇幅划分。
在小的逻辑上,会依据题目为切入点,先剖析实现思路,同时也会举一反三类似的题目,最初给出解答(解答可能不是最好、最准确的,然而必定能通过测试用例)。
如果感兴趣的话,能够继续关注。
type-challenges 最根底的常识
谨严来说,TypeScript 由三个局部组成:类型、语法、工程。 type-challenges 的题目波及到的是类型局部,这部分除了变量、函数等根底类型的标注, 最重要的就是各类类型工具的实现和应用,所以这里再把类型工具波及到的最根底也是最重要的常识复述一下, 其余知识点在题目中再提及。
类型工具
在 type-challenges 的题目中绝大部分题目是实现一个合乎 XXX 性能的类型工具,所以理解类型工具很重要。TS 内置了像 Pick
Partial
Required
Record
等类型工具,他们都基于类型别名
和泛型
实现。
类型别名用于封装一组或者一个特定类型。
type A = string;// 抽离一组联结类型type StatusCode = 200 | 301 | 400 | 500 | 502;type PossibleDataTypes = string | number | (() => unknown);// 申明一个对象类型,就像接口那样type ObjType = { name: string; age: number;}
如果在类型别名的根底上增加泛型坑位,就成了类型工具。它的基本功能依然是创立类型,只不过工具类型可能承受泛型参数,实现更灵便的类型创立性能。从这个角度看,工具类型就像一个函数一样,泛型是入参,外部逻辑基于入参进行某些操作,再返回一个新的类型。
type Factory<T> = T | number | string;// 工具类型来做类型标注, 生成一个新的类型别名type FactoryWithBool = Factory<boolean>;const foo: FactoryWithBool = true;
在 TS 中,类型工具能够这样划分:
- 依照应用形式来划分,类型工具能够分成三类:操作符、关键字与专用语法。
- 依照应用目标来划分,类型工具能够分为 类型创立 与 类型平安爱护。
不论是依照应用形式还是应用目标划分,类型工具都须要基于传入的泛型进行操作,如果泛型是一个简略类型,类型工具须要晓得该泛型的类型, 如果泛型是一个对象,还须要晓得对象 key、 value(键值对) 的状况。 这就波及到了 索引类型
和 映射类型
。
索引类型
索引类型蕴含三个局部:
- 索引签名类型,用来疾速申明一个键值类型统一的类型构造。
- 索引类型查问,应用关键词
keyof
它能够将对象中的所有键转换为对应字面量类型,而后再组合成联结类型。 - 索引类型拜访,通过键的字面量类型拜访这个键对应的键值类型。
// 索引类型签名interface AllStringTypes { [key: string]: string;}// 索引类型查问interface Foo { linbudu: 1, 599: 2}type FooKeys = keyof AllStringTypes; // "linbudu" | 599// 索引类型拜访interface Foo { propA: number; propB: boolean; propC: string;}type PropLinbudu = Foo['propA']; // numbertype PropFoo = Foo[keyof Foo]; // number | boolean | string
映射类型
映射类型和 js 中的 map类似,用于 基于键名映射到键值类型, 通过关键词 in
能够拜访一个对象类型蕴含的属性名和值类型。
// 应用 keyof 取得这个对象类型的键名组成字面量联结类型, 而后通过 in 将这个联结类型的每一个成员映射进去.// 最终 Stringify 类型工具润饰过的类型的值都是 string 类型type Stringify<T> = { [K in keyof T]: string;};interface Foo { prop1: string; prop2: number; prop3: boolean; prop4: () => void;}type StringifiedFoo = Stringify<Foo>;// 等价于interface StringifiedFoo { prop1: string; prop2: string; prop3: string; prop4: string;}
下面的代码, 稍加扭转, 就能够实现对一个对象类型的复制。
// 这里的`T[K]`其实就是下面说到的索引类型拜访type Clone<T> = { [K in keyof T]: T[K];};
把握了类型工具和索引、映射类型,对于题目 Readonly - 实现 Readonly (7) 就很容易了。
Readonly - 实现 Readonly (7)
type-challenges 第7题
实现
依照下面说到的复制类型工具,再给属性增加 readonly
修饰词就能够了
type Readonly<T> = { readonly [K in keyof T]: T[K]}
拓展: Required、Partial 的实现
Required、Partial 是一对工具类型, 他们性能上是相同的, 一个是润饰对象类型的属性都必须申明, 一个润饰对象类型的属性为可选申明。在 ts 对象类型中, 应用 ?
润饰属性,能够申明该属性为可选属性,应用 -?
润饰属性,能够申明该属性为必须属性, 那么 Required、Partial 实现如下:
type Partial<T> = { [K in keyof T]?: T[K]}type Required<T> = { [K in keyof T]-?: T[K]}
Pick - 实现 Pick(4)
type-challenges 第4题
题目
不应用 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,}
前置常识: 条件类型
TS 中有一套类型层级,从最下面一层的 any 类型,到最底层的 never 类型, 这些类型从上到下是有兼容性的。
TS 中应用 extends 判断类型的兼容性(继承),而非判断类型的全等性,因为在类型层面中,对于可能进行赋值操作的两个变量,咱们并不需要它们的类型齐全相等,只须要具备兼容性
解答和剖析
在内置的 Pick 类型工具中, 个别 K 值须要在对象类型的属性列表中, 所以这里须要应用到 extends 继承、keyof 索引查问的常识。
in
操作符在 类型映射
曾经提到了, 之外,它也能够通过 key in object
的形式来判断 key 是否存在于 object 或其原型链上(返回 true 阐明存在),来进行类型守卫。
// keyof T 返回 T 的属性类型的联结类型 K 须要继承这个联结类型, 确保 K 在 T 的属性列表中type Pick<T, K extends keyof T> = { [P in K]: T[P]}
Tuple to Object - 元组转换为对象(11)
type-challenges 第11题
题目形容
将一个元组类型转换为对象类型,这个对象类型的键/值和元组中的元素对应。
例如:
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as consttype result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
剖析
比拟容易想到的是,借助索引类型、类型映射、继承的常识, 咱们能够很容易的把元组类型转换成对象类型。
题目中能够看到,传入TupleToObject的泛型 tuple 应用 as const
做了类型断言,这样会给类型增加 readonly 属性
因为元组是非凡的数组, 这里应用 extends any[]
做了类型限度
type TupleToObject<T extends readonly any[]> = { [K in X]: K}
剩下的就是确定 X 应该怎么申明了,X 须要示意的是 T 全副元素的内容。
在 TS 的结构化类型零碎中,采纳的是 鸭子类型, 也就是如果两个数据的构造是统一的,TS 就认为这两个类型是统一的,哪怕类型名称不同。元组是在不同索引地位定义数据类型,同样对象也能够通过设置类索引的属性设置数据类型。
// 在申明对象时type tuple = { 0: string, 1: number, 2: boolean}// 在 TS 的类型比拟中,用对象类型定义元组是成立的const tup: tuple = ['1', 1, true]
既然能够用对象示意元组,那么获取元组的元素类型应用索引类型拜访就能够了
type tuple = { [p: string]: number}type tup = ['1', 1, true]type stringTypes = tuple[string] // string// tup 能够看作是索引都是 number 的对象类型type allTypes = tup[number] // '1' | 1 | true
答案
type TupleToObject<T extends readonly any[]> = { [K in T[number]]: K}
题目中须要留神例子中的两个细节。
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
应用as const
做了一次类型断言,目标是将 tuple 中的元素转变成字符串字变量类型
, 如果没有进行类型断言,tuple 中元素将都会解析成string
类型.- 应用 TupleToObject 参数中应用
typeof
做了一次转换目标是将 tuple 转变成类型申明而不是值申明。
First of Array 第一个元素 (14)
type-challenges 第14题
题目
实现一个First<T>泛型,它承受一个数组T并返回它的第一个元素的类型。
例如:
type arr1 = ['a', 'b', 'c']type arr2 = [3, 2, 1]type head1 = First<arr1> // 应推导出 'a'type head2 = First<arr2> // 应推导出 3
剖析
对于 infer 的用法
TypeScript 中反对通过 infer 关键字来在条件类型中提取类型的某一部分信息, 艰深说 infer就是用来在条件类型中申明占位变量的。 记得必须在条件类型中。
例如拿到数组中的值。
type Swap<T extends any[]> = T extends [infer A, infer B] ? [B, A] : T;
下面代码, infer 在条件类型中申明了变量 A、 B, 并在返回值中颠倒了数组程序。
解答
// 应用解构解析出第一个,和其余数值, 联合 infer 获取到解析的值type First<T extends any[]> = T extends [infer A, ...infer rest] ? A : never
Length of Tuple 获取元组长度 (18)
type-challenges 第18题
题目形容
创立一个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 4type spaceXLength = Length<spaceX> // expected 5
剖析
这道题和第 11 题(元组转换成对象)类似,能够把元组看成是对象类型或者数组类型,这样的话元组类型上就会有一个 length
属性,代表元组的长度。
答案
// 如果只是满足题目中的用例, 这个是 OK 的type Length<T extends readonly any[]> = T['length'];// 因为 any extends any[] 是能够为真的,那么在 Length 工具类能够传入 any, 所以须要应用 `{length: infer A}` 构造类型做一下限定type Length<T extends readonly any[]> = T extends { length: infer A } ? A : never;
为什么 any extends any[]
成立?
抛去 TS 中的类型层级,咱们能够把 any 看作是能够是任何类型,那么如果在条件类型中 any 作为比拟值时, 个别状况下就只有两种可能,成立和不成立。 既然是两种可能,那么上面的 Result
也就有两种类型后果了。
type Result = any extends 'linbudu' ? 1 : 2; // 1 | 2
也有不个别的状况。那就是被比拟放是 unknown
类型。 因为 unknown
只容许赋值给申明为 any 或者 unknown 的类型,也就是 any 在 unknown 的子集里
type Result = any extends unknown ? 1 : 2; // 1// any 原本就是顶级类型type Result2 = unknown extends any ? 1 : 2; // 1
Exclude 实现 Exclude(43)
type-challenges 第43题
题目
实现内置的 Exclude<T, U> 类型,但不能间接应用它自身。从联结类型 T 中排除 U 中的类型,来结构一个新的类型。
例如:
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
解析
Exclude 属于内置的汇合类工具类型,用来求出两个联结类型的差集, 除此之外还有交加、并集、补集 的内置工具。
实现这类汇合类的工具须要应用到 条件类型、条件类型分布式个性, 条件类型分布式的个性即当 T、U 都是联结类型(视为一个汇合)时,T 的成员会顺次被拿进去进行 extends U ? T1 : T2
的计算,而后将最终的后果再合并成联结类型。
答案
因为需要求出 T 、 U 之间的差集, 这里用 T 的成员顺次拿进去进行 extends U ? never : T
的计算,这样就能够把不是 U 子集的类型筛选进去,得出差集。
type MyExclude<T, U> = T extends U ? never : T;
交加 Extract 的实现
// 交加type Extract<T, U> = T extends U ? T : never;
Awaited 返回 Promise对象的类型 (189)
type-challenges 第189题
题目
如果咱们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,咱们用 Promise 中的 T 来形容这个 Promise 返回的类型。请你实现一个类型,能够获取这个类型。
例如:Promise<ExampleType>,请你返回 ExampleType 类型。
type ExampleType = Promise<string>type Result = MyAwaited<ExampleType> // string
解析
这道题还是须要用到 infer 的常识, 咱们能够应用 Promise<infer A>
的办法获取到 Promise
返回值类型
TS 中提供了一个合乎 Promise+ 标准的内置工具类型 PromiseLike
它蕴含了 .then
.catch
等办法, 咱们能够应用 PromiseLike<infer A>
更精确的获取。
答案
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer A> ? A extends PromiseLike<any> ? MyAwaited<A> : A: never;
解答中应用递归的模式又做了一次 A extends PromiseLike<any>
的判断, 这是因为用例中有嵌套的状况, 应用递归的模式就能够解决多层嵌套了。
type Z = Promise<Promise<string | number>>
If 实现 If(268)
type-challenges 第268题
实现一个 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, T, F> = C extends true ? T : F
Concat 实现 concat(533)
type-challenges 第533题
题目
在类型零碎里实现 JavaScript 内置的 Array.concat 办法,这个类型承受两个参数,返回的新数组类型应该依照输出参数从左到右的程序合并为一个新的数组。
type Result = Concat<[1], [2]> // expected to be [1, 2]
答案
这个比较简单, 间接应用解构的概念就能够了
// 用例中有上面的状况 所以须要应用增加 readonly // const tuple = [1] as const; // Concat<typeof tuple, typeof tuple>type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]
Includes 实现 Includes(898)
type-challenges 第898题
题目
在类型零碎里实现 JavaScript 的 Array.includes 办法,这个类型承受两个参数,返回的类型要么是 true 要么是 false。
例如:
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
解析
如果单从题目看的话,这个实现很简略。
依据 type-challenges 第11题, 能够把数组看作是属性是 number 的对象类型,那么就能够通过 T[number]
的模式拿到所有对象值类型的联结类型。
type Includes<T extends any[], U> = U extends T[number] ? true : false
然而从测试用例来看, 这个题目必定不属于简略的领域。还有以下简单场景是下面的实现不能满足的。
把下面的 U extends T[number]
表达式利用到用例中
// { a: 'A' } extends {} 是成立的,后果会是 trueExpect<Equal<Includes<[{}], { a: 'A' }>, false>>// false extends boolean 成立, 后果是 trueExpect<Equal<Includes<[boolean, 2, 3, 5, 6, 7], false>, false>>// boolean extends true boolean 能够拆解为 true 和 false, 所以这里的后果有两种可能 true | false // 如果是 true | false 形成的联结类型, 会被主动推导为 boolean 类型Expect<Equal<Includes<[true, 2, 3, 5, 6, 7], boolean>, false>>// { readonly a: 'A' } extends { a: 'A' } 成立, 只有指标类型的每个属性都能够在源类型中找到,并且相应属性具备雷同的属性值类型,那么就认为指标类型能够赋值给源类型。 并且 TypeScript 中的只读属性和可写属性能够相互赋值。 (可写属性不能赋值给只读属性)Expect<Equal<Includes<[{ a: 'A' }], { readonly a: 'A' }>, false>>// { a: 'A' } extends { readonly a: 'A' } 成立Expect<Equal<Includes<[{ readonly a: 'A' }], { a: 'A' }>, false>>// 1 | 2 extends 1 会别离用 1 extends 1 和 2 extends 1 进行计算, 后果收 true | false , ts 会推导为 booleanExpect<Equal<Includes<[1], 1 | 2>, false>>// 1 extends 1 | 2 成立Expect<Equal<Includes<[1 | 2], 1>, false>>
看完下面的用例,就发现最次要是要实现一个能跳过 TS 外部的类型变型,依照字面量级别的类型判断。 能够应用内置的 Equal 工具。 Equal(U, K) extends ? true : false
因为 T[number]
一个联结类型,应用 Equal 没法计算,能够借助 infer 占位符和递归的形式解析数组。
type Includes<T extends readonly any[], U> = T extends [infer A, ...infer rest] ? // 开始应用 Equal 进行单个比拟, 如果雷同就间接返回 true 否则就持续和剩下的元素进行比拟 (Equal<U, A> extends true ? true : Includes<rest, U>) : false;
Push、Unshift (3057, 3060)
type-challenges 第3057题
type-challenges 第3060题
题目
在类型零碎里实现通用的 Array.push 。实现类型版本的 Array.unshift。
例如:
type Result = Push<[1, 2], '3'> // [1, 2, '3']type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
解答
比较简单, 应用解构
type Push<T extends any[], U> = [...T, U];type Unshift<T extends any[], U> = [U, ...T];
Parameters 实现内置的 Parameters 类型 (3312)
type-challenges 第3312题
解答
同样是应用infer 作为占位,解析出返回值
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer A) => any ? A : never
总结
简略题目局部应用到的基础知识就是类型别名、类型工具、索引、映射类型、条件类型,infer 模式匹配,这些是 TS 类型的根底,倡议把 TypeScript 全面进阶指南 前 15 章肯定要认真品读。
另外 type-challenges 第898题 在 github 下面尽管有很多解答,然而短少解题思路和为什么要这样解答的思路,心愿能够通过这篇文章可能了解。