从java泛型来聊typescript泛型变量和泛型
Author:zhoulujun Date:
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
有了泛型之后,一个函数或容器类能处理的类型一下子扩到了无限大,似乎有点失控的感觉。所以这里又产生了一个约束的概念。我们可以声明对类型参数进行约束。
所以泛型,还是挺让人头晕的。本篇长文是本人看过的博客加自己的心得,整理出来的蛋炒饭!
泛型是什么?
泛型,即“参数化类型”。
一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
那么参数化类型怎么理解呢?
顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。
也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型之于类型(Types),犹类型之于变量
泛型为你提供了一种不用指定特别某种类型就能使用若干类型的方式。这给你的函数定义、类型定义,甚至接口定义赋予了更高一层的灵活性。
java泛型
泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。
Java 泛型是 J2 SE1.5 中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。—— 百度百科
更多可以参看:java 泛型详解、Java中的泛型方法、 java泛型详解
typescript泛型
说实话ts的文档我找了好几遍, 也没看到他给泛型正式做定义, 只是表达他是一种描述多种类型(类型范围)的格式, 我觉得有点抽象, 我用自己的理解具象下: 用动态的类型(泛型变量)描述函数和类的方式。
一些应用,这些应用充斥着各种重复类型定义, any 类型层出不穷,鼠标移到变量上面的提示只有 any,不要说类型操作了,类型能写对都是个问题。
真正的 TS 高手都是在玩类型,对类型进行各种运算生成新的类型。这也好理解,毕竟 TS 提供的其实就是类型系统。
平时我们都是对值进行编程,泛型是对类型进行编程。
你去看那些 TS 高手的代码,会各种花式使用泛型。 可以说泛型是一道坎,只有真正掌握它,你才知道原来 TS 还可以这么玩。
泛型的作用
泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。
相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。
用于解释泛型威力的典型例子,莫过于 identity 函数。该函数本质上只是原样返回你传入的唯一参数,别无他用,但如果你思考一下,如何在一种强类型语言中定义这样一个函数呢?
function identity(value: number):number { return value; }
上面的函数对于数字工作良好,那字符串呢?或布尔值?自定义类型又如何?在 TypeScript 中要覆盖所有可能性,明显只能选择 any 类型了:
function identity(value: any): any { return value }
这还挺行得通的,但此刻你的函数实际上丢失了所有类型的概念,你将不能在本该有确定类型信息的地方使用它们了。本质上来说现在你可以传入任何值而编译器将一言不发,这和你使用普通的 JavaScript 就没有区别了(即无论怎样都没有类型信息了):
let myVar = identity("Fernando") console.log(myVar.length) // 工作良好! myVar = identity(23) console.log(myVar.length) // 也能工作,尽管打印了 "undefined"
现在因为没有类型信息了,编译器就不能检查和函数相关的任何事情以及变量了,如此一来我们运行出了一个意外的 “undefined”(若将本例推及有更多逻辑复杂逻辑的真实场景将很可能变成最糟糕的一段程序)。
我们该如何修复这点并避免使用 any 类型呢?
TypeScript 泛型来拯救
一个泛型就像若干类型的一个变量,这意味着我们可以定义一个表示任何类型的变量,同时能保持住类型信息。后者是关键,因为那正是 any 做不到的。基于这种想法,现在可以这样重构我们的 identity 函数:
function identity<T>(value: T): T { return value; }
用来表示泛型名字的可以是任意字母,你可以随意命名。
推荐泛型中参数化类型(T,U,V,W)的命名约定
java常用的类型参数名称有:
E元素(Java集合框架广泛使用)
K键
N—编号
T型
V值
S、 U、V等-第2、3、4类
我想说,上面列出的Java命名约定与我认为的TypeScript泛型类型参数的实际命名约定非常一致:使用单个大写字符,或者对应于它们所表示的第一个字母,例如:
T为“type”,是最通用的,因此也是最常用的类型参数名;
K表示“key”,或P表示“property”,两者都倾向于受PropertyKey或keyof T或keyof SomeInterface或keyof SomeClass的约束;
V表示“value”,最常用作成对使用,K表示“key”;
A表示“arguments”,R表示“return”,分别对应函数签名的rest参数列表和返回类型,如(...args: A) => R;
N表示“number”,S表示“string”,B表示“boolean”,表示受原语约束的类型参数;
或一些相关类型的序列,例如:
T、U、V、W等等,从T开始表示“类型”,然后在需要更多类型时遍历字母表,记住这样只能得到一些类型;
A、B、C、D等等,从字母表的开头开始,当您希望使用一大堆类型参数,而您还没有将类型参数用于其他对象时。
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:
类的实例成员
类的方法
函数参数
函数返回值
泛型类型
我们可以用泛型变量去描述一个类型(类型范围), ts的数组类型Array
本身就是一个泛型类型, 他需要传递具体的类型才能变的精准:
let arr : Array<number>; arr = ['123']; // 错误, 提示数组中只可以有number类型 arr = [123];
下面我们自己定义一个泛型类型, 就对开头的convert
函数定义:
function convert<T>(input:T):T{ return input; } // 定义泛型类型 interface Convert { <T>(input:T):T } // 验证下 let convert2:Convert = convert // 正确不报错
什么是"泛型变量"和"泛型"
变量的概念我们都知道, 可以表示任意数据, 泛型变量也一样, 可以表示任意类型:
// 在函数名后面用"<>"声明一个泛型变量 function convert<T>(input:T):T{ return input; }
convert中参数我们标记为类型T, 返回值也标记为T, 从而表示了: 函数的输入和输出的类型一致. 这样使用了"泛型变量"的函数叫做泛型函数, 那有"泛型类"吗?
注意: T是我随便定义的, 就和变量一样, 名字你可以随便起, 只是建议都是大写字母,比如U / RESULT.
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。
泛型类(Generic classes)
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。
在java里面最典型的就是各种容器类,如:List、Set、Map。
typescript内置泛型 有 REC
class Person<U> { who: U; constructor(who: U) { this.who = who; } say(code:U): string { return this.who + ' :i am ' + code; } }
在类名后面通过"<>"声明一个泛型变量U, 类的方法和属性都可以用这个U, 接下来我们使用下泛型类:
let a = new Person<string>('詹姆斯邦德'); a.say(007) // 错误, 会提示参数应该是个string a.say('007') // 正确
我们传入了泛型变量(string),告诉ts这个类的U是string类型, 通过Person的定义, 我们知道say方法的参数也是string类型, 所以a.say(007)会报错, 因为007是number. 多以我们可以通过传入泛型变量来约束泛型.
泛型不仅应用于函数签名,亦可用来定义你自己的泛型类。这提供了将通用逻辑封装进可复用构造中的能力
abstract class Animal { handle() { throw new Error("Not implemented") } } class Horse extends Animal{ color: string handle() { console.log("Riding the horse...") } } class Dog extends Animal{ name: string handle() { console.log("Feeding the dog...") } } class Handler<T extends Animal> { animal: T constructor(animal: T) { this.animal = animal } handle() { this.animal.handle() } } class DogHandler extends Handler<Dog> {} class HorseHandler extends Handler<Horse> {}
在本例中,我们定义了一个可以处理任意动物类型的处理类,虽说不用泛型也能做到,但使用泛型的益处在最后两行显而易见。这是因为借助泛型,处理类逻辑完全被封装进了一个泛型类中,从而我们可以约束类型并创建指定类型的类,这样的类只对动物类型生效。你也可以在此添加额外的行为,而类型信息也得以保留。
来自这个例子的另一个收获是,泛型可被约束为仅继承自指定的一组类型。正如你所见,T 只能是 Dog 或 Horse 而非其他。
泛型接口
泛型还可以用在接口上,也就是泛型接口:
interface Fn<V, M> { message: M value: V } function fn3<T, U>(value: T, message: U): Fn<T, U> { return { message, value } }
可变参数元组(Variadic Tuples)
概况来说,可变参数元组带来的,是用泛型定义某元组中一个可变的部分,默认情况下这部分什么都没有。
一个普通的元组定义将产生一个固定尺寸的数组,其所有元素都是预定义好的类型:
type MyTuple = [string, string, number] let myList:MyTuple = ["Fernando", "Doglio", 37]
现在,归功于泛型和可变参数元组,你可以这样做:
type MyTuple<T extends unknown[]> = [string, string, ...T, number] let myList:MyTuple<[boolean, number]> = ["Fernando", "Doglio", true, 3, 37] let myList:MyTuple<[number, number, number]> = ["Fernando", "Doglio", 1,2,3,4]
如果你注意看,我们使用了一个泛型 T(继承自一个 unknown 数组)用以将一个可变部分置于元组中。因为 T 是 unknown 类型的一个列表,你可以在里面装任何东西。比分说,你可以将其定义为单一类型的一个列表,就像这样:
type anotherTuple<T extends number[]> = [boolean, ...T, boolean]; let oneNumber: anotherTuple<[number]> = [true, 1, true]; let twoNumbers: anotherTuple<[number, number]> = [true, 1, 2, true] let manyNumbers: anotherTuple<[number, number, number, number]> = [true, 1, 2, 3, 4, true]
自动推断泛型变量的类型
其实我们也可以不指定泛型变量为string, 因为ts可以根据实例化时传入的参数的类型推断出U为string类型:
let a = new Person('詹姆斯邦德'); // 等价 let a = new Person<string>('詹姆斯邦德'); a.say(007) // 错误, 会提示参数应该是个string a.say('007') // 正确
泛型方法
其实方法和函数的定义方式一样:
class ABC{ // 输入T[], 返回T getFirst<T>(data:T[]):T{ return data[0]; } }
泛型接口
通过传入不同的类型参数, 让属性更灵活:
interface Goods<T>{ id:number; title: string; size: T; } let apple:Goods<string> = {id:1,title: '苹果', size: 'large'}; let shoes:Goods<number> = {id:1,title: '苹果', size: 43};
扩展泛型变量(泛型约束)
function echo<T>(input: T): T { console.log(input.name); // 报错, T上不确定是否由name属性 return input; }
前面说过T可以代表任意类型, 但对应的都是基础类型, 所以当我们操作input.name
的时候就需要标记input上有name属性, 这样就相当于我们缩小了泛型变量的范围, 对泛型进行了约束:
// 现在T是个有name属性的类型 function echo<T extends {name:string}>(input: T): T { console.log(input.name); // 正确 return input; }
一个泛型的应用, 工厂函数
function create<T,U>(O: {new(): T|U; }): T|U { return new O(); }
主要想说3个知识点:
可以定义多个泛型变量.
泛型变量和普通类型用法一直, 也支持联合类型/交叉类型等类型.
如果一个数据是可以实例化的, 我们可以用
{new(): any}
表示.
不要乱用泛型
泛型主要是为了约束, 或者说缩小类型范围, 如果不能约束功能, 就代表不需要用泛型:
function convert<T>(input:T[]):number{ return input.length; }
这样用泛型就没有什么意义了, 和any
类型没有什么区别.
参考文章:
一文读懂 TypeScript 泛型及应用 https://juejin.cn/post/6844904184894980104
TypeScript泛型(使用泛型变量,泛型语法、泛型约束) https://www.cnblogs.com/goloving/p/15425328.html
再学 TypeScript (4)泛型 https://segmentfault.com/a/1190000038312096
https://www.tslang.cn/docs/handbook/generics.html
TypeScript:一个好泛型的价值 https://juejin.cn/post/6878868818836488205
为vue3学点typescript, 泛型 https://juejin.cn/post/6844904008587411463
转载本站文章《从java泛型来聊typescript泛型变量和泛型》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/typescript/2021_1130_8716.html