猪都能做的TypeScript面试题

2,380 阅读6分钟

猪都能做的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.tsconnected 的类型与:

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>;
};

该类型一个对象类型的,里面有特定的函数方法

  1. 函数名
  2. 函数类型

那我们要做的就是

  1. 获取EffectModule上的函数名
  2. 把获取到函数转换成新的签名

思路应该就是

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' ,具体看The keyof 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'的联合类型。

这里只是我们的需求恰好keyvalue两者相等,把这种易混淆的值替换掉

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]XXXretkey值集合

于是我们就可以写成

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子句中,可以通过inferU语句中声明一个推断型的变量,在UY语句中则可以引用此类推断的类型变量。

这有点像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]>;
};