猪都能做的TypeScript面试题!!! 这个是我同事让我改成这个名的 哈哈哈哈
TypeScript 面试题
背景
最近刚好在学习TS,虽然最讨厌面试啥的,但是通过比较好的面试题来学习,会更好的汲取某个知识的重点。
就是写着玩,权当记录。
一道 leetcode招前端的面试题。
问题定义
假设有一个叫 EffectModule
的类
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>
interface Action<T> {
payload?: T;
type: string;
}
type asyncMethod<T, U> = (input: Promise<T>) => Promise<Action<U>>;
type syncMethod<T, U> = (action: Action<T>) => Action<U>;
这个对象上还可能有一些任意的非函数属性:
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>
type asyncMethod<T, U> = (input: Promise<T>) => Promise<Action<U>>;
type newAsyncMethod<T, U> = (input: T) => Action<U>;
type syncMethod<T, U> = (action: Action<T>) => Action<U>;
type newSyncMethod<T, U> = (action: T) => Action<U>;
例子:
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)
要求
在 题目链接 里面的 index.ts
文件中,有一个 type Connect = (module: EffectModule) => any
,将 any
替换成题目的解答,让编译能够顺利通过,并且 index.ts
中 connected
的类型与:
type Connected = {
delay(input: number): Action<string>;
setMessage(action: Date): Action<number>;
}
完全匹配。
题目代码
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 的类型,让 connected 的类型变成预期的类型
type Connect = (module: EffectModule) => any;
const connect: Connect = m => ({
delay: (input: number) => ({
type: 'delay',
payload: `hello 2`
}),
setMessage: (input: Date) => ({
type: "set-message",
payload: input.getMilliseconds()
})
});
type Connected = {
delay(input: number): Action<string>;
setMessage(action: Date): Action<number>;
};
export const connected: Connected = connect(new EffectModule());
解析
其实要做的就是把第28行中any
替换为通过类型推导得到一个符合Connected
的类型
// 修改 Connect 的类型,让 connected 的类型变成预期的类型
type Connect = (module: EffectModule) => any
变成
type Connect = (module: EffectModule) => {
delay(input: number): Action<string>;
setMessage(action: Date): Action<number>;
}
当然,这里我们的结果是直接复制Connected
的,这里要做的就是用类型推导的方式来计算出这个结果来。
所谓的Connexted
类型就是: 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>;
仔细看这个Connected
type Connected = {
delay(input: number): Action<string>;
setMessage(action: Date): Action<number>;
};
该类型一个对象类型的,里面有特定的函数方法
- 函数名
- 函数类型
那我们要做的就是
- 获取
EffectModule
上的函数名 - 把获取到函数转换成新的签名
思路应该就是
type Connected = {
[N in EffectModule 方法名的集合]: 转换函数(EffectModule[N])
};
1. 获取函数名: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",
};
}
}
// 获取 EffectModule 类型中的函数名
type PickMethods<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type EffectModuleMethods = PickMethods<EffectModule>; // "delay" | "setMessage"
1.1 class声明即可以当做值也可以当做类型
我们平时说定义一个类型一般都是这么做的
type Person = {
name: string;
age: number;
};
const p1: Person = {
age: 18,
name: "wcdaren",
};
在上面的代码中:
Person
是一个类型p1
是一个值
而在class
中的使用中
class Person {
constructor(public name: string, public age: number) {}
}
const p2: Person = {
age: 18,
name: "wcdaren",
};
const p3 = new Person("wcdaren", 18);
在p2
声明时,Person
是作为一个类型来使用,类似上面的type Person
而在p3
声明时,Person
是作为一个类来实例化出一个对象,我们把这种可以不属于类型系统中的方式称为值的使用。
1.2 Mapped types
我们经常会遇到一个这样的需求:把一个类型中的属性统统设为可选的
interface Person {
name: string;
age: number;
}
转换为
interface PartialPerson {
name?: string;
age?: number;
}
很明显,最终的结果是依据老代码形成新代码,这种方式很容易让我们想到JS
中的一个数组函数Array.map
let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };
let users = [john, pete, mary];
/*
usersMapped = [
{ fullName: "John Smith", id: 1 },
{ fullName: "Pete Hunt", id: 2 },
{ fullName: "Mary Key", id: 3 }
]
*/
let usersMapped = users.map((user) => ({
fullName: `${user.name} ${user.surname}`,
id: user.id,
}));
在TS也提供了类似的方法,那就是Mapped Type
,具体的使用方式为:
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
这里拿Partial
举例
interface Person {
name: string;
age: number;
}
/**
{
name?: string | undefined;
age?: number | undefined;
}
*/
type PartialPerson = Partial<Person>;
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 伪代码
type Partial<'name'|'age'> = {
['name']?: Person['name'];
['age']?: Person['age'];
}
keyof
就是获取对象类型的key
值,并返回一个联合类型,即'name'|'age'
,具体看Thekeyof
type operator
1.3 Conditional Types
在Partial
的源代码中,通过Mapped types
我们确实是遍历出每个对象的属性,并在每个属性后添加?
,使得该属性成为可选属性
type Partial<T> = {
[P in keyof T]?: T[P];
};
而现在我们的需求是挑选出属性中为方法的属性,这就需要使用到Conditional Types
,而在讲Conditional Types
之前,需要提前明白 Generic Constraints,即泛型约束:简单来说就是我们可以通过extends
关键词来限制泛型的类型范围
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
// ^ Property 'length' does not exist on type 'T'.
return arg;
}
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
<T extends Lengthwise>
即表明传递进来的泛型T
是一个符合Lengthwise
的类型。
这个时候再来看T extends U ? X : Y
,就很容易理解成T
符合不符合类型U
,如果不符合就返回X
否则返回Y
。
具体到我们刚刚的题目,我们就要判断类的属性是不是一个函数,如果是一个函数我们就正常返回函数名字K
;如果不是的话,就删除掉,即返回一个never
type PickMethods<T> = { [K in keyof T]: T[K] extends Function ? K : never };
1.4 Indexed Access Types
到现在我们代码得到的结果是一个对象类型
type EffectModuleMethods = {
count: never;
message: never;
delay: "delay";
setMessage: "setMessage";
}
我们的目标是获得一个‘delay’|'setMessage'
的联合类型。
这里只是我们的需求恰好key
和value
两者相等,把这种易混淆的值替换掉
type ret = {
delay:'A';
setMessage:'B';
}
这里实际上我们要的是‘A’|'B'
这里的ret
是一个类型,假设他是一个对象的话,我们就会这么写
const ret = {
delay: "A",
setMessage: "B",
};
const endRet = [ret.delay, ret.setMessage];
console.log(`==============>endRet`);
console.log(endRet); // [ 'A', 'B' ]
上面代码中的
ret.delay
ret.setMessage
这种属性的访问方式,在JS中还可以写成
ret["delay"]
ret["setMessage"]
const ret = {
delay: "A",
setMessage: "B",
};
const endRet = [ret["delay"], ret["setMessage"]];
console.log(`==============>endRet`);
console.log(endRet); // [ 'A', 'B' ]
而对于类型来说,我们也可以这么来访问这种key-value
形式值,那这种访问方式就是Indexed Access Types
type ret = {
delay: "A";
setMessage: "B";
};
type endRet = ret["delay"] | ret["setMessage"]; // "A"|"B"
在上面的访问中ret[XXX]
,XXX
是ret
的key
值集合
于是我们就可以写成
type ret = {
delay: "A";
setMessage: "B";
};
type endRet = ret[keyof ret]; // "A"|"B"
于是我们就有最终的答案了
// 获取 EffectModule 类型中的函数名
type PickMethods<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type EffectModuleMethods = PickMethods<EffectModule>; // "delay" | "setMessage"
2. 把旧方法转换为新方法: 转换函数()
type asyncMethod<T, U> = (input: Promise<T>) => Promise<Action<U>>;
type newAsyncMethod<T, U> = (input: T) => Action<U>;
type syncMethod<T, U> = (action: Action<T>) => Action<U>;
type newSyncMethod<T, U> = (action: T) => Action<U>;
type MethodsTransformation<T> = T extends asyncMethod<infer A, infer B>
? newAsyncMethod<A, B>
: T extends syncMethod<infer C, infer D>
? newSyncMethod<C, D>
: never;
2.1 Type inference in conditional types
在 Utility Types 中有这么一个工具类型——ReturnType,用来返回一个函数返回值的类型。
declare function f1(): { a: number; b: string };
type T0 = ReturnType<() => string>;
// ^ = type T0 = string
type T1 = ReturnType<(s: string) => void>;
// ^ = type T1 = void
type T2 = ReturnType<<T>() => T>;
// ^ = type T2 = unknown
type T3 = ReturnType<<T extends U, U extends number[]>() => T>;
// ^ = type T3 = number[]
type T4 = ReturnType<typeof f1>;
// ^ = type T4 = {
// a: number;
// b: string;
// }
那这个是怎么实现的呢? 我们来看下源码:
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
首先,<T extends (...args: any) => any>
这个是前面已经说过的 Generic Constraints 泛型约束,限制传递进来的泛型是一个函数类型;
然后,这个T extends (...args: any) => infer R ? R : any
也是前面讲到的Conditional Types条件类型,即T extends U ? X : Y
。唯一不同的是,这里多了一个infer
关键词。
按官方文档的意思就是:在条件类型的T extends U ? X : Y
子句中,可以通过infer
在U
语句中声明一个推断型的变量,在U
和Y
语句中则可以引用此类推断的类型变量。
这有点像Vue
中的 slots 插槽。infer A
时,A
就像一个容器,承载将要传递进来的内容,即类型;接着就可以在X : Y
中使用该容器A
做为内容分发的出口,即把原本承载的类型信息返回。
懒得写下去了,靠悟吧!打工人回家吃便当了。
3. 返回最终结果
// 获取函数名
type PickMethods<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type EffectModuleMethods = PickMethods<EffectModule>; // "delay" | "setMessage"
// 把旧方法转换为新方法
type asyncMethod<T, U> = (input: Promise<T>) => Promise<Action<U>>;
type newAsyncMethod<T, U> = (input: T) => Action<U>;
type syncMethod<T, U> = (action: Action<T>) => Action<U>;
type newSyncMethod<T, U> = (action: T) => Action<U>;
type MethodsTransformation<T> = T extends asyncMethod<infer A, infer B>
? newAsyncMethod<A, B>
: T extends syncMethod<infer C, infer D>
? newSyncMethod<C, D>
: never;
// 修改 Connect 的类型,让 connected 的类型变成预期的类型
type Connect = (
module: EffectModule
) => {
[N in EffectModuleMethods]: MethodsTransformation<EffectModule[N]>;
};