面试官:你会TypeScript类型体操吗?(一)
起因
TypeScript
是我之前就一直有在进行使用。但对他的理解过低导致时不时就会出现一大堆红色波浪线,最终导致的就是my code
中充满了我们最喜欢的any
。因此咧,我最近下定决心好好研究一下TypeScript
。研究了两天冴羽大大的TS中文翻译文档(好记性不如烂笔头),我决定拿起在我star
列表中吃灰已久的github
项目type-challenges。下面我会把主要需要了解的ts基础列出再配合项目中的一些easy
题目进行讲解。
基础知识
extends
extends
主要用于类型约束以及类似于js三元表达式中的表达式部分,代码示例如下:
约束功能
// 对数组的length没有任何限制
type LengthIsAny = {
length:number
}
// 对数组的length限制为1
type LengthIsOne = {
length:1
}
function Check<T extends LengthIsAny>(arg:T):void{}
function Check1<T extends LengthIsOne>(arg:T):void{}
Check([1] as const)
Check([] as const)
Check1([] as const) //类型“readonly []”的参数不能赋给类型“LengthIsOne”的参数。
//属性“length”的类型不兼容。
// 不能将类型“0”分配给类型“1”。
Check1([1] as const)
类似三元表达式
当T的类型能够符合extends
右边的类型判断,走true
分支,否则走false
分支
type GetBool<T> = T extends string?true:false
type GetFlase = GetBool<1> //false
type GetTrue = GetBool<''> //true
对于上面代码的理解,可以类比于js的函数,T为函数参数,对T进行了处理之后返回了我们需要的新类型
infer (泛型推断)
在我做type-challenges都是配合上extends
的条件判断进行使用,符合类型推断的会将类型赋值给对应占位的infer后定义的类型U简单示例如下:
type GetTypeInfer<T> = T extends {type:infer I} ? I : never
type GetString = GetTypeInfer<{
type:string
}> // type GetString = string
type GetNumber = GetTypeInfer<{
type:number;
value:number
}> // type GetNumber = number
type GetNever = GetTypeInfer<{
value:number
}> // type GetNever = never
热身题
我们先从热身题中了解一下题目的解答格式,界面中分为了你的代码
测试用例
以及最上方的题目介绍
Hello World
type cases = [
Expect<NotAny<HelloWorld>>,
Expect<Equal<HelloWorld, string>>,
]
上面的Equal
是用用于对比两个类型是否等同,等同返回true
,否则返回false
而Expect
就是需要传入一个true才通过类型检查
答案
type HelloWorld = string // expected to be a string
简单题
4・实现 Pick
描述
从类型 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 keyof T as Key extends K ? Key:never ]:T[Key]
}
解析
keyof T
拿到了T
的所有key
值的联合类型title|completed
,在通过in
进行键名的遍历时,通过as对键名进行重新设置,其中通过extends
进行判断,如果不符合我们的类型K
,则设置为never
(如果键名设置为never,改行类型不会出现在最终结果,类似于js的delete
),符合则复值为Key
7・实现 Readonly
描述
该 Readonly
会接收一个 泛型参数,并返回一个完全一样的类型,只是所有属性都会被 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]
}
解析
和第一题一样进行了遍历,但是在computed key
的[]前加上了readonly
就能为所有类型加上只读属性
11・元组转换为对象
描述
传入一个元组类型,将这个元组类型转换为对象类型,这个对象类型的键/值都是从元组中遍历出来。
例如:
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 (string|number)[]> = {
[K in T[number]]: K
}
解析
T[number]
匹配了所有元组的元素=》T[0]、T[1]...
,所以返回了T
所有元素组成的联合类型,例如描述中的tuple
的T[number]
结果就是'tesla'| 'model 3'| 'model X'| 'model Y'
,所以K in T[number]
的结果就是每一次都取出T
的一个元素,type
的值也取K
。对T extends readonly (string|number)[]
进行限制是因为,通过as const
进行定义的数组的每一个元素都被限定为readonly
且需要限制对象的键名只能为string|number
,除外的都是不合法的(通过测试的最后一个示例)
14・第一个元素
描述
实现一个通用First<T>
,它接受一个数组T
并返回它的第一个元素的类型。
例如:
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
答案
type First<T extends unknown[]> = T extends [infer F, ...infer Rest] ? F : never
这里就用到了我们前面说的infer
,这里可以理解成对传入的类型T
进行解构赋值,第一个元素赋值给F
,剩余的类型数组赋值给Rest
,只要T能够正确extends
,那么走三元的true
分支,返回之前赋值的类型F
解析
18・获取元组长度
描述
创建一个通用的Length
,接受一个readonly
的数组,返回这个数组的长度。
例如:
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 Length<T extends readonly unknown[]> = T['length']
解析
这题很容易,数组的类型都能够直接访问length
属性,不要忘记as const
修饰的数组需要使用readonly进行类型限制
43・Exclude
描述
实现内置的Exclude <T, U>类型,但不能直接使用它本身。
从联合类型T中排除U的类型成员,来构造一个新的类型。
例如:
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
答案
type MyExclude<T, U> = T extends U ? never : T
解析
首先对于联合类型,我们要有一个类型分发的概念,T extends U ? never : T
并不只执行一次,而是根据T
与U
类型联合数量决定的,在上面的Result
中,T
是'a' | 'b' | 'c'
联合数3,'a'
是1,需要对这俩进行排列组合,所以是3次判断,分别为 'a' extends 'a' ? never : 'a'
|'b' extends 'a' ? never : 'b'
|'c' extends 'a' ? never : 'c'
=>得到结果'b' | 'c'
189・Awaited
描述
假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。
例如:Promise<ExampleType>
,请你返回 ExampleType 类型。
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
答案
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer U>?U extends Promise<any>?MyAwaited<U>:U:never
解析
对于这道题,我们首先要对传入的类型T
进行限制,保证他至少是符合Promise<unknown>
的,而且从测试示例中可以看到,可能存在Promise
嵌套Promise
的情况,所以我们需要一个递归调用本MyAwaited
进行处理,对于这种符合某种结构的类型,我们想要取得结构中的某个类型时,此时应该使用的就是infer
进行类型推断赋值给U
,如果判断U
是否为Promise
的嵌套情况,如果是,递归调用MyAwaited
,如果不是,则可以直接返回类型U
结语
这一篇到这就结束了,如果写的有什么不对的地方,希望能够帮忙指出。关于这个ts挑战的进度,我按顺序做到了 ,后续会分为多篇都写一下(作为自己的笔记哈哈哈哈)。
转载自:https://juejin.cn/post/7144746876766535688