泛型是在调用时再限定类型
我们在定义泛型的时候,是一系列类型变量,如 T 、 U 等,这些变量实际的类型我们在定义的时候是不知道的,只有在进行泛型调用的时候,由用户给定实际的类型,所以这里有一种延迟声明类型的作用。
泛型是否也有多个类型变量?
那么,既然泛型可以看做是 “类型的函数”,那么函数能接收多个参数的话,我们的泛型也能接收多个类型变量,比如:
function getTutureTutorialsInfo<T, U>(info: T[], profile: U): T[] {
console.log(info.length);
console.log(profile);
return info;
}
getTutureTutorialsInfo<string, object>(['hello tuture'], { username: 'tuture'})
可以看到,我们修改了 getTutureTutorialsInfo 函数的泛型定义,添加了一个新的类型变量 U ,并用 U 来注解了函数的第二个参数 profile 的类型。
同样,在调用 getTutureTutorialsInfo 函数的时候,我们也需要传入两个类型变量,这里我们的 profile 被认为是一个 object 类型。
匿名函数泛型?
在之前的内容中,我们通过命名函数来讲解了泛型,那么匿名函数如何使用泛型了?其实和命名函数类似,只不过匿名函数是如下形式:
const getTutureTutorialsInfo: <T>(info: T[]) => T[] = (info) => {
console.log(info.length);
return info;
}
// 或者
const getTutureTutorialsInfo: <T>(info: T[]) => T[] = function (info) {
console.log(info.length);
return info;
}
我们直接给匿名函数被赋值的变量进行匿名函数的注解,并加上泛型,你应该回想起之前给一个变量注解函数类型时的样子:
(args1: type1, args2: type2, ..., args3: type3) => returnType
而匿名函数泛型只不过在之前加上了 <T> 类型变量,然后可以用于注解参数和返回值。
泛型默认类型参数?
既然我们声称泛型是关于 “类型的函数”,为了更加深刻的论证我们这个观点,我们再进一步。
我们都知道函数存在默认参数一说,那么作为 “类型的函数” - 泛型,是否也有默认类型参数这一说了?不好意思,还真的有!我们来看个例子:
function getTutureTutorialsInfo<T, U = number>(info: T[], profile: U): T[] {
console.log(info.length);
console.log(profile);
return info;
}
getTutureTutorialsInfo<string, string>(['hello world'], 'hello tuture')
可以看到我们给类型变量 U 一个默认的类型参数 number (还记得 ES6 里面有默认值的参数必须靠后放置嘛?)
之后我们在进行泛型调用的时候,却给 U 传了 string 类型,把这段代码放到 src/index.ts 里面,应该不会报错,并且编辑器里面有良好的提示:
泛型,继续前进
接下来我们继续深入泛型,解答之前文章里的一些疑问,比如:
- 泛型数组
- 类泛型
同时我们还会了解一些新的概念,比如:
- 接口泛型
- 类型别名泛型
- 泛型约束
解决遗留的问题
泛型数组
这个我们已经在上面的例子中用到了,泛型实际上定义了一系列类型变量,然后我们可以对这些类型变量做任意的组合以适应各种不同的类型注解需求,其中一个组合例子就是泛型数组 - 某个类型变量的数组形态,也就是我们上面提到的 info: T[] ,其中 T[] 就是泛型数组。
当然泛型数组的表达形式还有另外一种:
Array<T>
即以泛型调用的形式返回一个关于泛型变量 T 的数组类型。所以我们的 getTutureTutorialsInfo 函数可以写成如下样子:
function getTutureTutorialsInfo<T>(info: Array<T>): Array<T> {
console.log(info.length);
return info;
}
getTutureTutorialsInfo<string>(['hello tuture', 'hello world'])
类泛型
类泛型的形式和函数泛型类似,我们来看一个类泛型的定义的调用,在 src/index.ts 里面额外添加下面的内容:
// 上面是 getTutureTutorialsInfo 泛型函数的定义和调用
class Tuture<T> {
info: T[];
}
let tutorial = new Tuture<string>()
tutorial.info = ['hello world', 'hello tuture'];
类泛型的定义也是在类名之后添加 <T> 这样的形式,然后就可以在类中使用 T 类型变量来注解类型。而类泛型的调用和函数泛型的调用类似。
学习了类泛型,我们再来解析一下在上一篇文章中提到的那个 TodoInput 组件,类似下面这样:
class TodoInput extends React.Component<TodoInputProps, TodoInputState> {
// ... 组件内容
}
这个实际上分为两个部分,首先是 React.Component 组件基类的类泛型调用,然后是 TodoInput 集成自这个类泛型。因为派生类 TodoInput 可以获取到父类的属性和方法,所以在 TodoInput 中使用的 this.props 和 this.state 在被类型注解之后,就可以在编码时自动补全
开启新篇章
了解了函数泛型、类泛型,你有可能有一点想法了关于泛型,是不是我们之前的很多讲解过的内容,如类型别名、接口等。你想对了!TS 会在尽可能多的地方,能用泛型就用上泛型,因为泛型可以将代码组件化,方便复用,所有智能的编译器,能不让你多写的东西,就绝对不会让你多写,通通用泛型给整上。
接口泛型
在了解接口泛型之前,我们先来看一个接口是怎么写的,在 src/index.ts 里面添加如下代码:
interface Profile {
username: string;
nickName: string;
avatar: string;
age: string;
}
一般我们的 Profile 类似上面的内容,但是有时候有些字段会根据需求的不同而不同,比如 age 这个字段,有些人喜欢定义成数字类型 number ,有些人喜欢定义成字符串类型 string ,所以这又是一个延迟赋予类型的例子,可以借助泛型来解决,我们修改一下上面的代码:
interface Profile<T> {
username: string;
nickName: string;
avatar: string;
age: T;
}
type ProfileWithAge = Profile<string>
可以看到,接口泛型的声明和调用与函数、类泛型的类似,它允许你在接口里面定义一些属性,使用类型变量来注解,在调用时指明这个属性的类型。
类型别名泛型
因为在很多场景下,类型别名和接口充当类似的角色,所以在了解完接口泛型之后,我们有必要来了解学习一下类型别名如何结合泛型使用,和接口类似,将上面的接口泛型 Profile 用类型别名重写如下:
type Profile<T> = {
username: string;
nickName: string;
avatar: string;
age: T;
}
type ProfileWithAge = Profile<string>
可以看到,基本一致!
泛型约束
我们来解决之前的一个遗留问题,那就是即使我使用了泛型,我还是不知道某个被泛型的类型变量注解的变量的一个结构是怎么样的即:
function getTutureTutorialsInfo<T, U>(info: T[], profile: U): T[] {
console.log(info.length);
console.log(profile);
return info;
}
getTutureTutorialsInfo<string, object>(['hello tuture'], { username: 'tuture'})
上面我们用类型变量 U 注解了 profile 参数,但我们在使用 profile 的时候,依然不知道它是什么类型,也就是说泛型虽然解决了类型的可复用性,但是还是不能让我们写代码时获得自动补全的能力
重申:没有补全的 TypeScript 代码是没有生命的!
那么我们如何让在既使用泛型的同时,还能获得代码补全了?答案相信你也猜到了, 那就是我们这一节要讲的泛型约束。 我们修改 src/index.ts 里面的代码如下:
type Profile<T> = {
username: string;
nickName: string;
avatar: string;
age: T;
}
function getTutureTutorialsInfo<T, U extends Profile<string>>(info: T[], profile: U): T[] {
console.log(info.length);
console.log(profile);
return info;
}
可以看到,我们复用了之前定义的 getTutureTutorialsInfo 和 Profile ,但是在 getTutureTutorialsInfo 泛型中第二个类型变量做了点改进,之前只是单纯的 U ,现在是 U extends Profile<string> , Profile<string> 表示调用类型别名泛型生成一个 age 为 string 的新类型别名,然后通过 U extends ... 的方式,用 Profile<string> 来限制 U 的类型,也就是 U 必须至少包含 Profile<string> 的类型。
这个时候,我们在 VSCode 编辑器里面尝试输入 profile. ,应该可以神奇的发现,有了自动补全:
并且还能了解到 age 是 string 属性!
再次!有了代码补全的 TS 充满了活力 !
当然这里的用于约束的 Profile<string> 可以是一个类型别名,也可以是一个接口,也可以是一个类:
class Profile<T> {
username: string;
nickName: string;
avatar: string;
age: T;
}
// 或者
interface Profile<T> {
username: string;
nickName: string;
avatar: string;
age: T;
}
// 或者
type Profile<T> = {
username: string;
nickName: string;
avatar: string;
age: T;
}
更近一步,这里的用于约束类型变量的类型可以是一些更加高级的类型如联合类型、交叉类型等:
type Profile<T> = {
username: string;
nickName: string;
avatar: string;
age: T;
}
type Tuture = {
github: string;
remote: string[];
}
function getTutureTutorialsInfo<T, U extends Profile<string> & Tuture>(info: T[], profile: U): T[] {
console.log(info.length);
console.log(profile);
return info;
}
可以看到我们使用了 Profile<string> 和 Tuture 的交叉类型来约束 U
深入实践,注解构造函数
在了解泛型的基础知识,并且结合函数、接口、类型别名和类进行结合使用之后,相信你对如何使用泛型已经有了一点经验了。
而了解了泛型,你就可以开始尝试深入 TS 类型编程的世界了!接下来我们开始深入一下高阶的 TS 类型编程知识,并尝试讲解一些比较边缘情况如何进行类型注解。
我们需要一个 createInstance 函数,它接收一个类构造函数,然后返回此类的实例,并能在调用之后获得良好的代码补全提示(!很重要),并且此函数还需要有足够好的通用性能处理任意构造函数(!泛型) 。我们尝试在 src/index.ts 里面编写一个类以及一个创建此类实例的方法:
class Profile<T> {
username: string;
nickName: string;
avatar: string;
age: T;
}
class TutureProfile extends Profile<string> {
github: string;
remote: string[];
}
function createInstance(B) {
return new B();
}
const myTutureProfile = createInstance(TutureProfile);
不要问我为什么
createInstance的参数是B,因为我们最后很new B()。
当我们编写了上面这个 createInstance 时,当我们尝试在调用之后输入 . : createInstance(TutureProfile). ,发现编辑器里面没有补全提示实例化对象的相关属性如 username 等
首先我们来解析一下构造函数的样子,因为 TS 类型是鸭子类型,是基于代码的实际样子来进行类型注解的。构造函数是可被实例化的函数,即可以通过 new XXX() 进行调用来创建一个实例,所以构造函数的注解应该类似这样:
interface ConstructorFunction<C> {
new (): C;
}
即形如 new (): C 的函数形式,表示可以通过调用 new XXX() 生成一个 XXX 的实例。即某个类:
class Profile<T> {
username: string;
nickName: string;
avatar: string;
age: T;
}
我们注解其构造函数类似下面:
const profileConstructor: ConstructorFunction<Profile<string>> = Profile;
这里有同学还记得嘛,我们在上一篇文章中讲到一个类在声明的时候会声明两个东西:1)用于注解此类实例的类型 2)以及此类的构造函数。这个例子是用来表达类在声明时声明的这两样东西的最佳例子之一即:
ConstructorFunction接口泛型接收的C用来注解new ()生成的实例,此为第一:用于注解此类实例的类型。- 用于注解
Profile的构造函数的类型ConstructorFunction<Profile<string>>,在注解profileConstructor变量之后,其初始化赋值是Profile本身,并且你可以在你的 VSCode 编辑器里面编写上面的代码,应该不会报错,这说明了第二:声明了此类的构造函数。
了解了构造函数如何进行类型注解之后,我们来完成第三点要求,让这个 createInstance 更具通用性,二话不说,泛型走起!最终代码如下:
class Profile<T> {
username: string;
nickName: string;
avatar: string;
age: T;
}
class TutureProfile extends Profile<string> {
github: string;
remote: string[];
}
interface ConstructorFunction<C> {
new (): C;
}
function createInstance<A extends Profile<string>>(B: ConstructorFunction<A>) {
return new B();
}
const myTutureProfile = createInstance(TutureProfile);
现在你在 VSCode 编辑器 createInstance(TutureProfile) 后面输入 . 应该可以看到代码补全:
这个例子其实关于 extends 类型约束那一块有点多余,但是为了组合我们在这一篇里面学到的知识,所以我额外把它也加上了,可以看到我们重拾了所有的代码补全,代码补全
上面类中如
remote等属性会有红色下划线是因为报了Property 'remote' has no initializer and is not definitely assigned in the constructor.ts(2564),字面意思就是没有初始化这些属性,这个不重要,可以通过配置移除,也可以初始化。It's your choice!