|
| 1 | +--- |
| 2 | +title: 想去力扣当前端,TypeScript 需要掌握到什么程度? |
| 3 | +tags: [前端, TypeScript] |
| 4 | +categories: |
| 5 | + - [前端] |
| 6 | + - [TypeScript] |
| 7 | +--- |
| 8 | + |
| 9 | +2018 年底的时候,力扣发布了岗位招聘,其中就有前端,仓库地址: https://github.com/LeetCode-OpenSource/hire 。与大多数 JD 不同, 其提供了 5 道题, 并注明了 `完成一个或多个面试题,获取免第一轮面试的面试机会。完成的题目越多,质量越高,在面试中的加分更多。完成后的代码可以任意形式发送给 [email protected]。以上几个问题完成一个或多个都有可能获得面试机会,具体情况取决于提交给我们的代码。` |
| 10 | + |
| 11 | + |
| 12 | + |
| 13 | +(力扣中国前端工程师 JD) |
| 14 | + |
| 15 | +今天我们就来看下第二题:`编写复杂的 TypeScript 类型`。通过这道题来看下, TypeScript 究竟要到什么水平才能进力扣当前端? |
| 16 | + |
| 17 | +> 其它四道题也蛮有意思的,值得一看。 |
| 18 | +
|
| 19 | +<!-- more --> |
| 20 | + |
| 21 | +## 问题描述 |
| 22 | + |
| 23 | +假设有一个叫 `EffectModule` 的类 |
| 24 | + |
| 25 | +```ts |
| 26 | +class EffectModule {} |
| 27 | +``` |
| 28 | + |
| 29 | +这个对象上的方法**只可能**有两种类型签名: |
| 30 | + |
| 31 | +```ts |
| 32 | +interface Action<T> { |
| 33 | + payload?: T |
| 34 | + type: string |
| 35 | +} |
| 36 | + |
| 37 | +asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>> |
| 38 | + |
| 39 | +syncMethod<T, U>(action: Action<T>): Action<U> |
| 40 | +``` |
| 41 | + |
| 42 | +这个对象上还可能有一些任意的**非函数属性**: |
| 43 | + |
| 44 | +```ts |
| 45 | +interface Action<T> { |
| 46 | + payload?: T; |
| 47 | + type: string; |
| 48 | +} |
| 49 | + |
| 50 | +class EffectModule { |
| 51 | + count = 1; |
| 52 | + message = "hello!"; |
| 53 | + |
| 54 | + delay(input: Promise<number>) { |
| 55 | + return input.then((i) => ({ |
| 56 | + payload: `hello ${i}!`, |
| 57 | + type: "delay", |
| 58 | + })); |
| 59 | + } |
| 60 | + |
| 61 | + setMessage(action: Action<Date>) { |
| 62 | + return { |
| 63 | + payload: action.payload!.getMilliseconds(), |
| 64 | + type: "set-message", |
| 65 | + }; |
| 66 | + } |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +现在有一个叫 `connect` 的函数,它接受 EffectModule 实例,将它变成另一个对象,这个对象上只有**EffectModule 的同名方法**,但是方法的类型签名被改变了: |
| 71 | + |
| 72 | +```ts |
| 73 | +asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>> 变成了 |
| 74 | +asyncMethod<T, U>(input: T): Action<U> |
| 75 | +``` |
| 76 | + |
| 77 | +```ts |
| 78 | +syncMethod<T, U>(action: Action<T>): Action<U> 变成了 |
| 79 | +syncMethod<T, U>(action: T): Action<U> |
| 80 | +``` |
| 81 | + |
| 82 | +例子: |
| 83 | + |
| 84 | +EffectModule 定义如下: |
| 85 | + |
| 86 | +```ts |
| 87 | +interface Action<T> { |
| 88 | + payload?: T; |
| 89 | + type: string; |
| 90 | +} |
| 91 | + |
| 92 | +class EffectModule { |
| 93 | + count = 1; |
| 94 | + message = "hello!"; |
| 95 | + |
| 96 | + delay(input: Promise<number>) { |
| 97 | + return input.then((i) => ({ |
| 98 | + payload: `hello ${i}!`, |
| 99 | + type: "delay", |
| 100 | + })); |
| 101 | + } |
| 102 | + |
| 103 | + setMessage(action: Action<Date>) { |
| 104 | + return { |
| 105 | + payload: action.payload!.getMilliseconds(), |
| 106 | + type: "set-message", |
| 107 | + }; |
| 108 | + } |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +connect 之后: |
| 113 | + |
| 114 | +```ts |
| 115 | +type Connected = { |
| 116 | + delay(input: number): Action<string>; |
| 117 | + setMessage(action: Date): Action<number>; |
| 118 | +}; |
| 119 | +const effectModule = new EffectModule(); |
| 120 | +const connected: Connected = connect(effectModule); |
| 121 | +``` |
| 122 | + |
| 123 | +要求: |
| 124 | + |
| 125 | +在 [题目链接](https://codesandbox.io/s/4tmtp "题目链接") 里面的 `index.ts` 文件中,有一个 `type Connect = (module: EffectModule) => any`,将 `any` 替换成题目的解答,让编译能够顺利通过,并且 `index.ts` 中 `connected` 的类型与: |
| 126 | + |
| 127 | +```typescript |
| 128 | +type Connected = { |
| 129 | + delay(input: number): Action<string>; |
| 130 | + setMessage(action: Date): Action<number>; |
| 131 | +}; |
| 132 | +``` |
| 133 | + |
| 134 | +**完全匹配**。 |
| 135 | + |
| 136 | +> 以上是官方题目描述,下面我的补充 |
| 137 | +
|
| 138 | +上文提到的`index.ts` 比 题目描述多了两个语句,它们分别是: |
| 139 | + |
| 140 | + |
| 141 | + |
| 142 | +(题目额外信息) |
| 143 | + |
| 144 | +## 思路 |
| 145 | + |
| 146 | +首先来解读下题目。 题目要求我们补充类型 `Connect` 的定义, 也就是将 any 替换为不报错的其他代码。 |
| 147 | + |
| 148 | +回顾一下题目信息: |
| 149 | + |
| 150 | +- 有一个叫 `connect` 的函数,它接受 EffectModule 实例,将它变成另一个对象,这个对象上只有**EffectModule 的同名方法**,但是方法的类型签名被改变了 |
| 151 | +- 这个对象上还可能有一些任意的**非函数属性** |
| 152 | +- 这个对象(EffectModule 实例)上的方法**只可能**有两种类型签名 |
| 153 | + |
| 154 | +根据以上信息,我们能够得到:`我们只需要将作为参数传递进来的 EffectModule 实例上的函数类型签名修改一下,非函数属性去掉即可`。所以,我们有两件问题要解决: |
| 155 | + |
| 156 | +1. 如何将非函数属性去掉 |
| 157 | +2. 如何转换函数类型签名 |
| 158 | + |
| 159 | +### 如何将非函数属性去掉 |
| 160 | + |
| 161 | +我们需要定义一个泛型,功能是接受一个对象,如果对象的 value 是 函数,则保留,否则去掉即可。不懂泛型的朋友可以先看下我之前写的文章: [你不知道的 TypeScript 泛型(万字长文,建议收藏)](https://lucifer.ren/blog/2020/06/16/ts-generics/ "你不知道的 TypeScript 泛型(万字长文,建议收藏)") |
| 162 | + |
| 163 | +这让我想起了官方提供的 Omit 泛型 `Omit<T,K>`。举个例子: |
| 164 | + |
| 165 | +```ts |
| 166 | +interface Todo { |
| 167 | + title: string; |
| 168 | + description: string; |
| 169 | + completed: boolean; |
| 170 | +} |
| 171 | + |
| 172 | +type TodoPreview = Omit<Todo, "description">; |
| 173 | + |
| 174 | +// description 属性没了 |
| 175 | +const todo: TodoPreview = { |
| 176 | + title: "Clean room", |
| 177 | + completed: false, |
| 178 | +}; |
| 179 | +``` |
| 180 | + |
| 181 | +官方的 Omit 实现: |
| 182 | + |
| 183 | +```ts |
| 184 | +type Pick<T, K extends keyof T> = { |
| 185 | + [P in K]: T[P]; |
| 186 | +}; |
| 187 | +type Exclude<T, U> = T extends U ? never : T; |
| 188 | +type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; |
| 189 | +``` |
| 190 | + |
| 191 | +实际上我们要做的就是 Omit 的变种,不是 Omit 某些 key,而是 Omit 值为非函数的 key。 |
| 192 | + |
| 193 | +由于 Omit 非函数实际就就是 Pick 函数,并且无需显式指定 key,因此我们的泛型只接受一个参数即可。 于是模仿官方的 `Pick` 写出了如下代码: |
| 194 | + |
| 195 | +```ts |
| 196 | +// 获取值为函数的 key,形如: 'funcKeyA' | 'funcKeyB' |
| 197 | +type PickFuncKeys<T> = { |
| 198 | + [K in keyof T]: T[K] extends Function ? K : never; |
| 199 | +}[keyof T]; |
| 200 | + |
| 201 | +// 获取值为函数的 key value 对,形如: { 'funcKeyA': ..., 'funKeyB': ...} |
| 202 | +type PickFunc<T> = Pick<T, PickFuncKeys<T>>; |
| 203 | +``` |
| 204 | + |
| 205 | +使用效果: |
| 206 | + |
| 207 | +```ts |
| 208 | +interface Todo { |
| 209 | + title: string; |
| 210 | + description: string; |
| 211 | + addTodo(): string; |
| 212 | +} |
| 213 | + |
| 214 | +type AddTodo = PickFunc<Todo>; |
| 215 | + |
| 216 | +const todo: AddTodo = { |
| 217 | + addTodo() { |
| 218 | + return "关注脑洞前端~"; |
| 219 | + }, |
| 220 | +}; |
| 221 | + |
| 222 | +type ADDTodoKey = PickFuncKeys<Todo>; // 'addTodo' |
| 223 | +``` |
| 224 | + |
| 225 | +可以看出,PickFunc 只提取了函数属性,忽略了非函数属性。 |
| 226 | + |
| 227 | +### 如何转换函数类型签名 |
| 228 | + |
| 229 | +我们再来回顾一下题目要求: |
| 230 | + |
| 231 | + |
| 232 | + |
| 233 | +也就是我们需要知道**怎么才能提取 Promise 和 Action 泛型中的值**。 |
| 234 | + |
| 235 | +实际上这两个几乎一样,会了一个,另外一个也就会了。我们先来看下 `Promise`。 |
| 236 | + |
| 237 | +从: |
| 238 | + |
| 239 | +```ts |
| 240 | +(arg: Promise<T>) => Promise<U> |
| 241 | +``` |
| 242 | + |
| 243 | +变为: |
| 244 | + |
| 245 | +```ts |
| 246 | +(arg: T) => U; |
| 247 | +``` |
| 248 | + |
| 249 | +如果想要完成这个需求,需要借助`infer`。只需要在类型前加一个关键字前缀 `infer`,TS 会将推导出的类型自动填充进去。 |
| 250 | + |
| 251 | +infer 最早出现在此 [官方 PR](https://github.com/Microsoft/TypeScript/pull/21496) 中,表示在 extends 条件语句中待推断的类型变量。 |
| 252 | + |
| 253 | +简单示例如下: |
| 254 | + |
| 255 | +```ts |
| 256 | +type ParamType<T> = T extends (param: infer P) => any ? P : T; |
| 257 | +``` |
| 258 | + |
| 259 | +在这个条件语句 `T extends (param: infer P) => any ? P : T` 中,infer P 表示待推断的函数参数。 |
| 260 | + |
| 261 | +整句表示为:如果 T 能赋值给 (param: infer P) => any,则结果是 (param: infer P) => any 类型中的参数 P,否则返回为 T。 |
| 262 | + |
| 263 | +一个更具体的例子: |
| 264 | + |
| 265 | +```ts |
| 266 | +interface User { |
| 267 | + name: string; |
| 268 | + age: number; |
| 269 | +} |
| 270 | + |
| 271 | +type Func = (user: User) => void; |
| 272 | + |
| 273 | +type Param = ParamType<Func>; // Param = User |
| 274 | +type AA = ParamType<string>; // string |
| 275 | +``` |
| 276 | + |
| 277 | +这些知识已经够我们用了。 更多用法可以参考 [深入理解 TypeScript - infer](https://jkchao.github.io/typescript-book-chinese/tips/infer.html#%E4%BB%8B%E7%BB%8D "深入理解 TypeScript - infer") 。 |
| 278 | + |
| 279 | +根据上面的知识,不难写出如下代码: |
| 280 | + |
| 281 | +```ts |
| 282 | +type ExtractPromise<P> = { |
| 283 | + [K in PickFuncKeys<P>]: P[K] extends ( |
| 284 | + arg: Promise<infer T> |
| 285 | + ) => Promise<infer U> |
| 286 | + ? (arg: T) => U |
| 287 | + : never; |
| 288 | +}; |
| 289 | +``` |
| 290 | + |
| 291 | +提取 Action 的 代码也是类似: |
| 292 | + |
| 293 | +```ts |
| 294 | +type ExtractAction<P> = { |
| 295 | + [K in keyof PickFunc<P>]: P[K] extends ( |
| 296 | + arg: Action<infer T> |
| 297 | + ) => Action<infer U> |
| 298 | + ? (arg: T) => Action<U> |
| 299 | + : never; |
| 300 | +}; |
| 301 | +``` |
| 302 | + |
| 303 | +至此我们已经解决了全部两个问题,完整代码见下方代码区。 |
| 304 | + |
| 305 | +## 关键点 |
| 306 | + |
| 307 | +- 泛型 |
| 308 | +- extends 做类型约束 |
| 309 | +- infer 做类型提取 |
| 310 | +- 内置基本范型的使用和实现 |
| 311 | + |
| 312 | +## 代码 |
| 313 | + |
| 314 | +我们将这几个点串起来,不难写出如下最终代码: |
| 315 | + |
| 316 | +```ts |
| 317 | +type ExtractContainer<P> = { |
| 318 | + [K in PickFuncKeys<P>]: |
| 319 | + P[K] extends (arg: Promise<infer T>) => Promise<infer U> ? (arg: T) => U : |
| 320 | + P[K] extends (arg: Action<infer T>) => Action<infer U> ? (arg: T) => Action<U> : |
| 321 | + never |
| 322 | +type Connect = (module: EffectModule) => ExtractContainer<EffectModule> |
| 323 | +``` |
| 324 | +
|
| 325 | +完整代码在我的 [Gist](https://gist.github.com/azl397985856/5aecb2e221dc1b9b15af34680acb6ccf "Gist 地址") 上。 |
| 326 | +
|
| 327 | +## 总结 |
| 328 | +
|
| 329 | +我们先对问题进行定义,然后分解问题为:`1. 如何将非函数属性去掉`, `2. 如何转换函数类型签名`。最后从分解的问题,以及基础泛型工具入手,联系到可能用到的语法。 |
| 330 | +
|
| 331 | +这个题目不算难,最多只是中等。但是你可能也看出来了,其不仅仅是考一个语法和 API 而已,而是考综合实力。这点在其他四道题体现地尤为明显。这种考察方式能真正考察一个人的综合实力,背题是背不来的。我个人在面试别人的时候也非常喜欢问这种问题。 |
| 332 | +
|
| 333 | +只有**掌握基础 + 解决问题的思维方法**,面对复杂问题才能从容不迫,手到擒来。 |
| 334 | +
|
| 335 | +大家也可以关注我的公众号《脑洞前端》获取更多更新鲜的前端硬核文章,带你认识你不知道的前端。 |
| 336 | +
|
| 337 | + |
| 338 | +
|
| 339 | +知乎专栏【 [Lucifer - 知乎](https://www.zhihu.com/people/lu-xiao-13-70 " Lucifer - 知乎")】 |
| 340 | +
|
| 341 | +点关注,不迷路! |
0 commit comments