Skip to content

Commit 1ec6719

Browse files
author
lucifer
committed
feat: 一大波更新
1 parent badf223 commit 1ec6719

11 files changed

+4678
-80
lines changed

Diff for: .docsifytopdfrc.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
contents: ["./docs/_sidebar.md"], // array of "table of contents" files path
3+
pathToPublic: "pdf/readme.pdf", // path where pdf will stored
4+
mainMdFilename: "README.md",
5+
pathToStatic: "./docs",
6+
pathToDocsifyEntryPoint: "./docs",
7+
removeTemp: true, // remove generated .md and .html or not
8+
emulateMedia: "screen", // mediaType, emulating by puppeteer for rendering pdf, 'print' by default (reference: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pageemulatemediamediatype)
9+
};

Diff for: docs/README.md

+81-80
Large diffs are not rendered by default.

Diff for: docs/topics/ts/leetcode-interview-ts.md

+341
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
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+
![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggfv55mufyj30u00wh0z0.jpg)
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+
![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggfvu17znfj30u80eiad2.jpg)
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+
![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggfy6bz4prj31mo0b8goj.jpg)
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+
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfxro1x125j30oz0dw43s.jpg)
338+
339+
知乎专栏【 [Lucifer - 知乎](https://www.zhihu.com/people/lu-xiao-13-70 " Lucifer - 知乎")】
340+
341+
点关注,不迷路!

0 commit comments

Comments
 (0)