昨天我们提到了泛型(generics)的使用,但泛型就像一个型别为 any
的变数一样,使用者爱带什麽型别都可以,基本上是没有型别上的限制,但有些时候我们想要使用泛型,让函式或 type alias 可以不只适用於一种型别,但有希望能对使用者带入的型别有一点限制的话,可以怎麽做呢?
在 TypeScript 中提供了「泛型限制(Generic Constraints)」的用法,语法上只需要使用 extends
就可以了!
一般 TypeScript 的初学者看到 extends
时,直觉上会想到的是可以拿来扩展某一个介面(interfaces)使用,像是这样:
interface Person {
age: number;
occupation: string;
}
// 使用 extends 来扩展另一个 interface
interface Author extends Person {
firstName: string;
lastName: string;
}
就可以建立一个新的 Author
interface,且让它带有 Person
中所定义的属性:
const aaron: Author = {
age: 33,
occupation: 'developer',
firstName: 'PJ',
lastName: 'Chen',
};
或者另一个很多人会想到的是 JavaScript 中「类别继承(class extends)」的使用,例如:
class Square {
constructor(public width: number) {}
}
// 使用 extends 来继承另一个 class 的属性
class Rectangle extends Square {
constructor(width: number, public height: number) {
super(width);
}
}
const square = new Square(10);
const rectangle = new Rectangle(10, 20);
然而,在 TypeScript 中的 extends
除了上述用法外,还被赋予了更多的功能,像是可以用来限制泛型可被带入的型别(generic constraints)或是作为型别的条件判断(conditional types)。在这种情况下,extends
比较好理解的中文应该是「需要满足 ooo」,但更精确的是指「是 ooo 的子集合」。今天就先来看一下如何透过 extends 来限制泛型可被带入的型别。
extends
在建立 Type Utility 是非常容易用到,因此我们在後面几天也会一直看到它。
先来看一下昨天写的函式:
function getFirstElement<T>(arr: T[]): T {
const [firstElement] = arr;
return firstElement;
}
假设现在我们希望限制这个 T 只能是数值(number)的话,可以搭配 extends 写成 <T extends number>
,意思就是限制使用者带入的泛型 「T 需要时 number 的子集合」:
更精确的来说,应该是指「T
要是 number
的子集合(subset)」,如果用集合的图示来表达的话,会像这样:
这时候如果我们在呼叫 getFirstElement
时,带入的却是 string[]
的话,TS 就会报错,因为 T 现在是 string,但 T 并是 number 的子集合:
画成图的概念会像这样:
同样的,如果是希望泛型 T 只能带入 string 或 number 的话,则可以写成 <T extends number | string>
,意思就是 T 这个泛型不能什麽都接受,它需要时 string 或 number 的子集合才行,像是这样:
这时候如果使用者带入的泛型不是 number 或 string 的话 TS 就会报错。例如,下图带的是 boolean:
到这里你可能虽然知道了「喔~原来 extends
还能当成『需要满足 ooo』」的意思,但却还不知道实际的使用时机。
关於这点我们会在後面几天看到很多实际的例子,这里先提供一个简单的范例,假设有一个函式可以输出姓名,它可以:
firstName
和 lastName
这两个属性一开始可能会这样写这个 function:
function logPersonName<T>(person: T) {
return `${person.firstName} ${person.lastName}`;
}
但这时候因为 TypeScript 没办法确保泛型 T
中一定有 firstName
和 lastName
这两个属性,因此会报错:
这时候就可以透过 generic constraints 的方式,限制使用者带入的泛型的型别至少要包含 firstName
和 lastName
这两个属性,其他的属性 TypeScript 则不管。
可以写成这样:
interface PersonName {
firstName: string;
lastName: string;
}
// 使用 T extends PersonName,限制 T 一定要是 PersonName 型别的子集合
function logPersonName<T extends PersonName>(person: T) {
return `${person.firstName} ${person.lastName}`;
}
这时候因为能够确保带入 function 参数的泛型 T 一定有 firstName
和 lastName
这两个属性,所以 TypeScript 就不会再报错,使用者也可以带入任何物件,只要这个物件中包含这两个必要的属性:
// 只要使用者带入的物件包含 firstName 和 lastName 就好(符合对泛型的限制)
// 其他多余的物件属性 TypeScript 不会管
logPersonName({
firstName: 'Aaron',
lastName: 'Chen',
occupation: 'developer',
});
logPersonName({
firstName: 'PJ',
lastName: 'Chen',
favorite: 'smart doctor',
});
但如果带入的物件少了 firstName
或 lastName
,则 TS 就会直接报错:
关於使用 extends
来限制泛型可被接受型别的用法同样适用在 type alias 上,例如:
type PersonNameType {
firstName: string;
lastName: string
}
type Person<T extends PersonNameType> = T;
意思一样是泛型 T
可以是任何型别,但它至少要是 PersonName
这个型别的子集合,也就是要有 firstName
和 lastName
这两个属性。使用时会像这样:
/**
* T 等於
* {
* firstName: string;
* lastName: string;
* occupation: string;
* }
* */
const pjchender: Person<{
firstName: string;
lastName: string;
occupation: string;
}> = {
firstName: 'PJ',
lastName: 'Chen',
occupation: 'developer',
};
後面我们会再看到更多例子,到时候会更清楚 extends
在泛型中的使用。
https://tsplay.dev/Wv89rN @ TypeScript Playground
资讯爆炸後,网站上线再也不会放着就有源源不绝的流量,网路上充斥着各种形形色色的网站,各种内容不断地被...
「欢迎来到 XX 的大家庭,希望大家把团队当作家人,一起成长……」 这是在某间公司报到时,HR 对我...
这一篇文章我们将要谈谈常常听到的 DataMapper 这个东西,应该是有不少人在一些 ORM 的 ...
todos: 还在出去玩,之後补上演算法 pesudocode + comments 8.2 提到的...
接着来更深入的了解数位影像的取样与量化吧! 取样简单来说就是我们要以多少个方格来表示这张图片,方格愈...