从一道面试题学TypeScript进阶技巧
最近中国Leetcode在招人,在网上公布了一套面试题,题型都很有水准,也比较有代表性。这篇文章就选取其中的TypeScript类型题分析一下解题需要运用的一些非常实用的TS进阶技巧。
题面
假设有一个叫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>
除了方法之外,这个对象还有任意的非函数属性。
现在有一个叫connect
的函数,接受EffectModule
实例将它变成另一个对象,这个对象上只有EffectModule
的同名方法,但是方法的类型签名有变化如下:
asyncMethod<T, U>(input: T): Action<U>
syncMethod<T, U>(action: T): Action<U>
现在需要完成connect
函数的类型定义满足题意。
可以看到,这题的关键点有两个。第一点,经过connect
函数调用之后,对象上只剩下方法,那么如何在类型中过滤掉非函数属性就是最先要考虑的。第二点就是如何映射方法的类型,将类型中的泛型提出了。
题解
首先第一步要取出函数方法类型名。不知道大家是否记得我在过去的文章中介绍过内置有一些有用的工具泛型,其中有个泛型名为Pick
,用于从某个类型中获取子类型,原始实现如下
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
这个思路和我们的需求是很像的,可以仿照着写一版获取自身函数方法名的Pick。第二个泛型就不需要了,因为需要的属性名都在第一个泛型T里,那么关键就在于如何从第一个泛型T中选取函数方法类型。这里就需要一个关键特性上场了:条件类型。当类型继承自函数类型时,保留不变,反之将其映射为never
类型,再通过keyof
属性选择过滤。实现如下:
type FuncNamePick<T> = {
[P in keyof T]: T[P] extends Function ? P : never;
}[keyof T];
使用该工具泛型,就可以完成EffectModule
中函数方法名的过滤。
type EffectModuleFuncNames = FuncNamePick<EffectModule>;
成功选出函数方法名后,就需要定义对函数方法的映射了。核心需求其实就是针对两种不同的函数类型,分别按照题意,将其中的泛型类型解析出来,替换掉原来的类型。那么首先还是用条件类型区分不同的两种函数类型,再通过infer
关键字获取到泛型类型,在映射类型中替换。可以实现一个类型如下:
type resolveEffectModuleFunc<T> =
T extends (input : Promise<infer U>) => Promise<Action<infer V>>
? (input: U) => Action<V>
: T extends (action: Action<infer U>) => Action<infer V>
? (action: U) => Action<V>
: never;
现在我们针对函数属性名和函数本身的类型处理都完成了,就可以结合写出最后的connect
类型了。
type Connect = (module: EffectModule) => {
[T in EffectModuleFuncNames]: resolveEffectModuleFunc<EffectModule[T]>
}
小结
通过这道面试题也能够初步窥见TS进阶特性的用法了。其实我的看法是,使用TS的核心不在于用到了多么艰深复杂的特性或者一些奇技淫巧实现各种类型,而在于真正领会到了TS的类型系统和JS的区别与联系——类型系统负责Type,JavaScript负责Script。事实上是可以完全区分开来的两套语法,不论什么TS特性都可以看作是只属于TypeScript的语法,而编程的基本思想和JS甚至其他语言都是一致的,只不过产出由“值”变为“类型”而已,泛型无非就是根据不同类型生成不同类型,不就是写计算类型的函数么。拿上面这个例子来说,resolveEffectModuleFunc还是有点复杂的,条件类型等于if判断,那么就是嵌套了几层判断逻辑,为了简洁和复用,我们也可以把其中几个类型抽出来,实现如下
type AsyncMethod<U, V> = (input : Promise<U>) => Promise<Action<V>>;
type AsyncMethod2<U, V> = (action: Action<infer U>) => Action<infer V>;
type resolveAsyncMethod<U, V> = (input: U) => Action<V>;
type resolveAsyncMethod2<U, V> = (action: U) => Action<V>;
type resolveEffectModuleFunc<T> =
T extends AsyncMethod<infer U, infer V>
? resolveAsyncMethod<U, V>
: T extends AsyncMethod2<infer U, infer V>
? resolveAsyncMethod2<U, V>
: never;
思路和平时编程是一样的,所以平时写TypeScript时,就把它当做一门新语言来写,将“类型”和“值”的概念区分清楚就可以了。
-- EOF --
前端开发
」下,并被添加
「TypeScript」
标签。