2018 年末的时候,力扣发布了岗位招聘,其中就有前端,仓库地址:https://github.com/LeetCode-OpenSource/hire 。与大多数 JD 不一样, 其提供了 5 道题, 并注明了: 完成一个或多个面试题,获取免第一轮面试的面试机会。完成的题目越多,质量越高,在面试中的加分更多。完成后的代码能够任意形式发送给 jobs@lingkou.com。以上几个问题完成一个或多个都有可能得到面试机会,具体状况取决于提交给咱们的代码。
html
(力扣中国前端工程师 JD)前端
今天咱们就来看下第二题:编写复杂的 TypeScript 类型
。经过这道题来看下, TypeScript 究竟要到什么水平才能进力扣当前端?git
❝其它四道题也蛮有意思的,值得一看。github
❞
假设有一个叫 EffectModule
的类web
class EffectModule {}
复制代码
这个对象上的方法「只可能」有两种类型签名:面试
interface Action<T> {
payload?: T type: string } asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>> syncMethod<T, U>(action: Action<T>): Action<U> 复制代码
这个对象上还可能有一些任意的「非函数属性」:typescript
interface Action<T> {
payload?: T; type: string; } class EffectModule { count = 1; message = "hello!"; delay(input: Promise<number>) { return input.then((i) => ({ payload: `hello ${i}!`, type: "delay", })); } setMessage(action: Action<Date>) { return { payload: action.payload!.getMilliseconds(), type: "set-message", }; } } 复制代码
如今有一个叫 connect
的函数,它接受 EffectModule 实例,将它变成另外一个对象,这个对象上只有「EffectModule 的同名方法」,可是方法的类型签名被改变了:前端工程师
asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>> 变成了
asyncMethod<T, U>(input: T): Action<U> 复制代码
syncMethod<T, U>(action: Action<T>): Action<U> 变成了 syncMethod<T, U>(action: T): Action<U> 复制代码
例子:async
EffectModule 定义以下:编辑器
interface Action<T> {
payload?: T; type: string; } class EffectModule { count = 1; message = "hello!"; delay(input: Promise<number>) { return input.then((i) => ({ payload: `hello ${i}!`, type: "delay", })); } setMessage(action: Action<Date>) { return { payload: action.payload!.getMilliseconds(), type: "set-message", }; } } 复制代码
connect 以后:
type Connected = {
delay(input: number): Action<string>; setMessage(action: Date): Action<number>; }; const effectModule = new EffectModule(); const connected: Connected = connect(effectModule); 复制代码
要求:
在 题目连接[1] 里面的 index.ts
文件中,有一个 type Connect = (module: EffectModule) => any
,将 any
替换成题目的解答,让编译可以顺利经过,而且 index.ts
中 connected
的类型与:
type Connected = {
delay(input: number): Action<string>; setMessage(action: Date): Action<number>; }; 复制代码
「彻底匹配」。
❝以上是官方题目描述,下面个人补充
❞
上文提到的index.ts
比 题目描述多了两个语句,它们分别是:
(题目额外信息)
首先来解读下题目。 题目要求咱们补充类型 Connect
的定义, 也就是将 any 替换为不报错的其余代码。
回顾一下题目信息:
connect
的函数,它接受 EffectModule 实例,将它变成另外一个对象,这个对象上只有
「EffectModule 的同名方法」,可是方法的类型签名被改变了
根据以上信息,咱们可以获得:咱们只须要将做为参数传递进来的 EffectModule 实例上的函数类型签名修改一下,非函数属性去掉便可
。因此,咱们有两件问题要解决:
咱们须要定义一个泛型,功能是接受一个对象,若是对象的 value 是 函数,则保留,不然去掉便可。不懂泛型的朋友能够先看下我以前写的文章: 你不知道的 TypeScript 泛型(万字长文,建议收藏)[2]
这让我想起了官方提供的 Omit 泛型 Omit<T,K>
。举个例子:
interface Todo {
title: string; description: string; completed: boolean; } type TodoPreview = Omit<Todo, "description">; // description 属性没了 const todo: TodoPreview = { title: "Clean room", completed: false, }; 复制代码
官方的 Omit 实现:
type Pick<T, K extends keyof T> = {
[P in K]: T[P]; }; type Exclude<T, U> = T extends U ? never : T; type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; 复制代码
实际上咱们要作的就是 Omit 的变种,不是 Omit 某些 key,而是 Omit 值为非函数的 key。
因为 Omit 非函数实际就就是 Pick 函数,而且无需显式指定 key,所以咱们的泛型只接受一个参数便可。 因而模仿官方的 Pick
写出了以下代码:
// 获取值为函数的 key,形如: 'funcKeyA' | 'funcKeyB'
type PickFuncKeys<T> = { [K in keyof T]: T[K] extends Function ? K : never; }[keyof T]; // 获取值为函数的 key value 对,形如: { 'funcKeyA': ..., 'funKeyB': ...} type PickFunc<T> = Pick<T, PickFuncKeys<T>>; 复制代码
使用效果:
interface Todo {
title: string; description: string; addTodo(): string; } type AddTodo = PickFunc<Todo>; const todo: AddTodo = { addTodo() { return "关注脑洞前端~"; }, }; type ADDTodoKey = PickFuncKeys<Todo>; // 'addTodo' 复制代码
能够看出,PickFunc 只提取了函数属性,忽略了非函数属性。
咱们再来回顾一下题目要求:
也就是咱们须要知道「怎么才能提取 Promise 和 Action 泛型中的值」。
实际上这两个几乎同样,会了一个,另一个也就会了。咱们先来看下 Promise
。
从:
(arg: Promise<T>) => Promise<U>
复制代码
变为:
(arg: T) => U;
复制代码
若是想要完成这个需求,须要借助infer
。只须要在类型前加一个关键字前缀 infer
,TS 会将推导出的类型自动填充进去。
infer 最先出如今此 官方 PR 中,表示在 extends 条件语句中待推断的类型变量。
简单示例以下:
type ParamType<T> = T extends (param: infer P) => any ? P : T;
复制代码
在这个条件语句 T extends (param: infer P) => any ? P : T
中,infer P 表示待推断的函数参数。
整句表示为:若是 T 能赋值给 (param: infer P) => any,则结果是 (param: infer P) => any 类型中的参数 P,不然返回为 T。
一个更具体的例子:
interface User {
name: string; age: number; } type Func = (user: User) => void; type Param = ParamType<Func>; // Param = User type AA = ParamType<string>; // string 复制代码
这些知识已经够咱们用了。 更多用法能够参考 深刻理解 TypeScript - infer[3] 。
根据上面的知识,不难写出以下代码:
type ExtractPromise<P> = {
[K in PickFuncKeys<P>]: P[K] extends ( arg: Promise<infer T> ) => Promise<infer U> ? (arg: T) => U : never; }; 复制代码
提取 Action 的 代码也是相似:
type ExtractAction<P> = {
[K in keyof PickFunc<P>]: P[K] extends ( arg: Action<infer T> ) => Action<infer U> ? (arg: T) => Action<U> : never; }; 复制代码
至此咱们已经解决了所有两个问题,完整代码见下方代码区。
咱们将这几个点串起来,不难写出以下最终代码:
type ExtractContainer<P> = {
[K in PickFuncKeys<P>]: P[K] extends (arg: Promise<infer T>) => Promise<infer U> ? (arg: T) => U : P[K] extends (arg: Action<infer T>) => Action<infer U> ? (arg: T) => Action<U> : never type Connect = (module: EffectModule) => ExtractContainer<EffectModule> 复制代码
完整代码在个人 Gist[4] 上。
咱们先对问题进行定义,而后分解问题为:1. 如何将非函数属性去掉
, 2. 如何转换函数类型签名
。最后从分解的问题,以及基础泛型工具入手,联系到可能用到的语法。
这个题目不算难,最多只是中等。可是你可能也看出来了,其不只仅是考一个语法和 API 而已,而是考综合实力。这点在其余四道题体现地尤其明显。这种考察方式能真正考察一我的的综合实力,背题是背不来的。我我的在面试别人的时候也很是喜欢问这种问题。
只有「掌握基础 + 解决问题的思惟方法」,面对复杂问题才能从容不迫,手到擒来。
你们也能够关注个人公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。
知乎专栏【 Lucifer - 知乎[5]】
点关注,不迷路!
题目连接: https://codesandbox.io/s/4tmtp
[2]你不知道的 TypeScript 泛型(万字长文,建议收藏): https://lucifer.ren/blog/2020/06/16/ts-generics/
[3]深刻理解 TypeScript - infer: https://jkchao.github.io/typescript-book-chinese/tips/infer.html#%E4%BB%8B%E7%BB%8D
[4]Gist 地址: https://gist.github.com/azl397985856/5aecb2e221dc1b9b15af34680acb6ccf
[5]Lucifer - 知乎: https://www.zhihu.com/people/lu-xiao-13-70