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