再次研究一道网红typescript面试题

5,327 阅读4分钟

题目描述

原题地址 有一个叫EffectModule的类

class EffectModule {}


这个对象上有各种各样的属性,string、number、function等,其中他的function类型的属性只可能有两种类型签名:

asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>
syncMethod<T, U>(action: Action<T>): Action<U>

现在有一个叫 connect 的函数,它接受 EffectModule实例,将它变成另一个对象,这个对象上只有EffectModule 的同名方法,但是方法的类型签名被改变了:

interface Action<T> {
  payload?: T
  type: string
}

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>

例子: 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之后:

// 问题就是把any换成解答,使得ts编译正常
// 也就是让类型Connect的返回值和Connected完全一样,ts编译通过
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>
}
const effectModule = new EffectModule()
const connected: Connected = connect(effectModule)

要求:type Connect = (module: EffectModule) => any,把any换成解答,让类型Connect的返回值和Connected完全一样,使得ts编译正常

很明显,我们需要做的事情就是:

  • 把EffectModule的函数类型取出来
  • 把函数的参数、返回值解promise/action

把EffectModule的函数类型取出来

说到取某些key出来,就是Pick或者Omit了。但ts又没有类似Object.keys().filter这种方式,需要使用映射类型+never去做特殊处理。整个流程就是:映射类型 =》 如果值为函数类型,返回key,否则返回never =》 对映射类型取值,得到函数类型的key

映射类型

是指把一个类型映射为另一个类型,key用的是类似for in的语法,表示遍历旧的类型里面每一个key:

type mapType0<T> = {
  [k in keyof T]: T[k]
}

在映射类型后面加上[keyof T],相当于valueof的方法了——返回的是这个类型里面所有的key的value的联合类型:

const o = {
    a: 1,
    b: '2'
}

type map1 = mapType0<typeof o>[keyof typeof o]
// string | number

Pick和Omit

我们知道了映射类型,基于此可以实现一个Pick(ts其实已经自带)

type myPick<T, K extends keyof T> = {
  [k in K]: T[k]
}

第二个泛型参数约束key来源于T,这样子就可以确保取得到原对象某些key了。基于Pick,就可以实现Omit

type MyOmit<T, K extends keyof any> 
= Pick<T, Exclude<keyof T, K>>;

获取value为function类型的key

type FunctionKeys<T> = {
  [k in keyof T]: T[k] extends Function ? k : never
}[keyof T]
type functionKeys = Pick<EffectModule, FunctionKeys<EffectModule>>

使用Omit实现

type noFunctionKeys<T> = {
  [k in keyof T]: T[k] extends Function ? never : k
}[keyof T]
type functionKeys = Omit<EffectModule, noFunctionKeys<EffectModule>>

现在,你得到了一个对象,只有value为函数类型的key了

把函数的参数、返回值解promise/action

infer

infer表示在condition type的条件语句中待推断的类型变量,可以理解为解方程,infer x表示x是待求解的变量,infer相当于一个标记作用。例如returntype就是靠infer实现的

type MyReturnType<T> = T extends (...args: any[]) => infer P ? P : any;

再看两个例子,比如解Promise、获取数组的item类型:

type UnPromisify<T> = T extends Promise<infer U> ? U : T
type UnArray<T> = T extends (infer U)[] ? U : T

实现效果

所以基于前面,我们可以把函数参数、返回值提取出来,解Promise、Action

type UnPromisify<T> = T extends Promise<infer U> ? U : T
type UnAction<T> = T extends Action<infer U> ? U : T

type MapTypeToUnPromisifyAndUnAction<T extends any[]> = {
  [k in keyof T]: UnAction<UnPromisify<T[k]>>
}

type Connect = (module: EffectModule) => ({
  [functionKey in keyof functionKeys]: (input: MapTypeToUnPromisifyAndUnAction<
    Parameters<functionKeys[functionKey]>
    >[number]) => UnPromisify<ReturnType<functionKeys[functionKey]>>
})

全部代码

type FunctionKeys<T> = { [k in keyof T]: T[k] extends Function ? k : never }[keyof T]
type functionKeys = Pick<EffectModule, FunctionKeys<EffectModule>>

type UnPromisify<T> = T extends Promise<infer U> ? U : T
type UnAction<T> = T extends Action<infer U> ? U : T

type MapTypeToUnPromisifyAndUnAction<T extends any[]> = {
  [k in keyof T]: UnAction<UnPromisify<T[k]>>
}

type Connect = (module: EffectModule) => ({
  [functionKey in keyof functionKeys]: (input: MapTypeToUnPromisifyAndUnAction<
    Parameters<functionKeys[functionKey]>
    >[number]) => UnPromisify<ReturnType<functionKeys[functionKey]>>
})