在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言: 我在面试的时候,通常喜欢问候选人一些莫名其妙的问题。比如这样的问题,假如你是某个库的作者,你如何实现某个功能。这类问题一般没有正确的答案,主要意图是考察一下候选人对这个库有没有更深入的理解,次要意图是觉得这样挺好玩。玩归玩,但该严肃的时候也要严肃起来。有一次,我面试到一位用过 这件事对候选人的影响可大可小,但对我的影响挺大的。它致使我一定要写出一篇关于泛型的文章。但自从种下这个种子后,我就开始后悔了。因为越接触TS中的泛型,越觉得这个题材没什么好写的。一来呢,TS中的泛型犹如空气,经常使用却难以描述。二者呢,它太过宽泛,难以面面俱到。 今天的这篇文章将不同于这个系列的以往。这篇文章将从C++模版要解决的问题出发,引出TS泛型要解决的问题,并简答介绍一些稍微高级的使用场景。 一、模版说起泛型,不得不提一下泛型的鼻祖,模版。C++中的模版以烧脑壳和强大著称,并被各类大牛津津乐道多年。就现在而言,Java、.NET或TS中的泛型都可以被认为是实现了C++模版的子集。对于子集的说法,我不敢苟同。因为就存在的目的而言,TS和C++模版完全不一样。 C++模版的出现是为了产生类型安全的通用容器。我们先来说一下通用容器,比如我写了个链表或者数组,这个数据结构不太关心存在里面的具体数据是什么类型,它都可以实现对应的操作。但js本身不关注类型和大小,所以js中的数组本来就是通用容器。对于TS而言,泛型的出现就可以解决这个问题。另一个值得对比的是产生,C++模版最终产出的是对应的类或函数,但对于TS而言,TS无法产生任何东西。有的同学可能要问了,TS不是最终产生JS代码吗?这样说有点不严谨,因为TS最终是分离出了JS代码,而没有对原有逻辑做任何处理。 C++模版的另一个目的就是元编程。这个元编程相当地强大,它主要通过编译时的程序设计构造来优化程序的执行。就TS而言,目前它只做了一处类似的优化,就是const enum可以内联在执行的地方,仅此而已。关于这类优化,上篇结束的位置也提到了基于类型推导的优化,但目前而言,TS还没有这个功能。倘若这类简单的优化都不支持,那对于更为复杂的元编程而言,就更不可能了(元编程需要对泛型参数进行逻辑推导,并最终内联到使用到的地方)。 关于C++模版,就说这么多吧,毕竟这不是一篇关于模版元编程的文章,而且我也不是专家,更多关于模版的问题,可以去问问轮子哥。说这么多模版,主要还是想说,TS中的泛型和模版是非常不一样的!如果你是从 二、泛型我认为TS中的泛型主要有3个主要用途:
关于第二、三点,因为之前文章已经很清楚地提到过,这里不再赘述。关于第一点,我这里举两个例子: 第一个例子是关于泛型容器,假如我想实现一个简单的泛型链表,代码如下: class LinkedList<T> { // 泛型类 value: T; next?: LinkedList<T>; // 可以使用自身进行类型声明 constructor(value: T, next?: LinkedList<T>) { this.value = value; this.next = next; } log() { if (this.next) { this.next.log(); } console.log(this.value); } } let list: LinkedList<number>; // 泛型特化为number [1, 2, 3].forEach(value => { list = new LinkedList(value, list); }); list.log(); // 1 2 3 第二个是泛型组件,假如我想实现一个通用的表单组件,可以这样写: function Form<T extends { [key: string]: any }>({ data }: { data: T }) { return ( <form> {data.map((value, key) => <input name={key} value={value} />)} </form> ) } 这个例子不止演示了泛型组件,也演示了如何使用extends定义泛型约束。现实中的泛型表单组件可能比这个更为复杂,上面只是演示一下思路。 到此为止,TS的泛型就讲完了!但这个文章还没完,下面我们来看一下泛型的一些高级使用技巧。 三、泛型递归递归简单来说就是函数的输出可以继续作为输入来进行逻辑演算的一类解决问题的思路。举个简单的例子,比如我们要算加法,定义了一个 在现实生活中,递归是如此的常见,以至于我们经常忽略它的存在。程序的世界也是如此。这里举个例子,并用这个例子来说明TS中的递归如何实现。比如,我现在有个泛型类型ReturnType<T>,它可以返回一个函数的返回类型。但我现在有个调用层级很深的函数,而且我不知道它的层级有多深,我该如何做呢? 思路一: type DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? DeepReturnType<ReturnType<T>> // 这里引用自身 : ReturnType<T>; 上面代码的说明:这里定义了一个 任何直观、简洁的方案背后都有一个但是。但是,这个是无法通过编译的。主要原因是,TS暂时不支持。以后支不支持我不知道,但,官方给的理由很明确:
所以,我们该如何实现这类需求呢?方法是有的,如官方给出的思路,我们可以使用有限次数的递归。下面给出我的思路: // 两层泛型类型 type ReturnType1<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? ReturnType<ReturnType<T>> : ReturnType<T>; // 三层泛型类型 type ReturnType2<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? ReturnType1<ReturnType<T>> : ReturnType<T>; // 四层泛型类型,可以满足绝大多数情况 type DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends ( ...args: any ) => any ? ReturnType2<ReturnType<T>> : ReturnType<T>; // 测试 const deep3Fn = () => () => () => () => "flag is win" as const; // 四层函数 type Returned = DeepReturnType<typeof deep3Fn>; // type Returned = "flag is win" const deep1Fn = () => "flag is win" as const; // 一层函数 type Returned = DeepReturnType<typeof deep1Fn>; // type Returned = "flag is win" 这种技巧可以推广到定义深层结构的 四、默认泛型参数有时候我们很喜欢泛型,但有时候我们又不希望类或函数的消费者每次都指定泛型的类型,这时候,我们可以使用默认的泛型参数。这个在很多第三方库中广泛使用,比如: // 接收P S C的泛型组件 class Component<P,S,C> { props: P; state: S; context:C .... } // 需要这样使用 class MyComponent extends Component<{}, {}, {}>{} // 但如果我的组件是个很纯粹的组件,并不需要props、state和context呢 // 可以这样定义 class Component<P = {}, S = {}, C = {}> { props: P; state: S; context:C .... } // 然后可以这么使用 class MyComponent extends Component {} 我觉得这个特性非常实用,它以一种js中很自然的方式实现了C++模版中的 五、泛型重载泛型重载在官方文档上提过几嘴,这种重载依赖于函数重载的一些机制,因此,我们先来看一下TS中的函数重载吧。这里,我用 const square = (n) => n * n; // 接收函数的map map({ 'a': 4, 'b': 8 }, square); // => [16, 64] (iteration order is not guaranteed) const users = [ { 'user': 'barney' }, { 'user': 'fred' } ]; // 接收string的map map(users, 'user'); // => ['barney', 'fred'] 那么,这样的类型声明如何在TS中表达呢?我可以使用函数重载,比如这样: // 这里只做演示,不保证正确性。真实场景下这里需要填充正确的类型,而不是any interface MapFn { (obj: any, prop: string): any; // 当接收string时的情况,情景一 (obj: any, fn: (value: any) => any): any; // 当接收函数时的情况,情景二 } const map: MapFn = () => ({}); map(users, 'user'); // 重载情景一 map({ 'a': 4, 'b': 8 }, square); // 重载情景二 上面这段代码使用了TS中比较奇特的一种机制,也就是函数、new等 类函数的定义可以写在 当然,也可以使用另一种更为传统的做法,比如下面这样: function map(obj: any, prop: string): any; function map(obj: any, fn: (value: any) => any): any; function map(obj, secondary): any {} 这里,基本讲清楚了函数重载。推广到泛型,基本上是一样的。这里举一个知友提的问题的例子,对于这个问题,这里不再赘述。解决思路大概是这样的: interface FN { (obj: { value: string; onChange: () => {} }): void; <T extends {[P in keyof T]: never}>(obj: T): void; // ,对于obj的类型T而言,它始终不接收其它的key。 } const fn: FN = () => {}; fn({}); // 正确 fn({ value: "Hi" }); // 错误 fn({ onChange: () => {} }); // 错误 fn({ value: "Hi", onChange: () => ({}) }); // 正确 对于React生态,这里有一个比较值得阅读的泛型重载的实例,那就是 整体而言,我不太喜欢这篇文章。究其原因,TS中的泛型使用广泛,因其设计初衷的原因,可玩性较差。但我对这种设计理念是支持的,首先,它能够满足我们定义类型的要求,其次,它做到了比C++模版更为简单易用。 到此这篇关于关于C++ TpeScript系列的泛型的文章就介绍到这了,更多相关TypeScript泛型内容请搜索极客世界以前的文章或继续浏览下面的相关文章希望大家以后多多支持极客世界! |
请发表评论