[Day03] TS:泛型就。很。泛!用 extends 来加上一点限制吧!

昨天我们提到了泛型(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);

使用 extends 来限制泛型可接受的型别

然而,在 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 的子集合」:

Generic Constraints

更精确的来说,应该是指「T 要是 number 的子集合(subset)」,如果用集合的图示来表达的话,会像这样:

Generic Constraints

这时候如果我们在呼叫 getFirstElement 时,带入的却是 string[] 的话,TS 就会报错,因为 T 现在是 string,但 T 并是 number 的子集合:

Generic Constraints

画成图的概念会像这样:

Generic Constraints

同样的,如果是希望泛型 T 只能带入 string 或 number 的话,则可以写成 <T extends number | string>,意思就是 T 这个泛型不能什麽都接受,它需要时 string 或 number 的子集合才行,像是这样:

Generic Constraints

这时候如果使用者带入的泛型不是 number 或 string 的话 TS 就会报错。例如,下图带的是 boolean:

Generic Constraints

到这里你可能虽然知道了「喔~原来 extends 还能当成『需要满足 ooo』」的意思,但却还不知道实际的使用时机。

关於这点我们会在後面几天看到很多实际的例子,这里先提供一个简单的范例,假设有一个函式可以输出姓名,它可以:

  1. 接受「任何型别的物件」当作参数
  2. 但因为它要输出姓名,所以参数本身有一个限制,就是物件中至少要有 firstNamelastName 这两个属性

一开始可能会这样写这个 function:

function logPersonName<T>(person: T) {
  return `${person.firstName} ${person.lastName}`;
}

但这时候因为 TypeScript 没办法确保泛型 T 中一定有 firstNamelastName 这两个属性,因此会报错:

Generic Constraints

这时候就可以透过 generic constraints 的方式,限制使用者带入的泛型的型别至少要包含 firstNamelastName 这两个属性,其他的属性 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 一定有 firstNamelastName 这两个属性,所以 TypeScript 就不会再报错,使用者也可以带入任何物件,只要这个物件中包含这两个必要的属性:

// 只要使用者带入的物件包含 firstName 和 lastName 就好(符合对泛型的限制)
// 其他多余的物件属性 TypeScript 不会管

logPersonName({
  firstName: 'Aaron',
  lastName: 'Chen',
  occupation: 'developer',
});

logPersonName({
  firstName: 'PJ',
  lastName: 'Chen',
  favorite: 'smart doctor',
});

但如果带入的物件少了 firstNamelastName ,则 TS 就会直接报错:

Generic Constraints

Type Alias 中的 Generics 中同样适用 extends 来限制泛型

关於使用 extends 来限制泛型可被接受型别的用法同样适用在 type alias 上,例如:

type PersonNameType {
  firstName: string;
  lastName: string
}

type Person<T extends PersonNameType> = T;

意思一样是泛型 T 可以是任何型别,但它至少要是 PersonName 这个型别的子集合,也就是要有 firstNamelastName 这两个属性。使用时会像这样:

/**
 * 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

参考资料

  • Generics @ TypeScript > Type Manipulations

<<:  组译器与连结器 (上)

>>:  4 游戏状态结构

网页上线,然後呢?Google五大免费成效追踪工具,带你主动出击!

资讯爆炸後,网站上线再也不会放着就有源源不绝的流量,网路上充斥着各种形形色色的网站,各种内容不断地被...

[Day28] 打造高效团队,先累积社会资本

「欢迎来到 XX 的大家庭,希望大家把团队当作家人,一起成长……」 这是在某间公司报到时,HR 对我...

30-18 之 DataSource Layer- DataMapper

这一篇文章我们将要谈谈常常听到的 DataMapper 这个东西,应该是有不少人在一些 ORM 的 ...

【Day 10】Concurrency control in apps

todos: 还在出去玩,之後补上演算法 pesudocode + comments 8.2 提到的...

[D04] 取样与量化(2)

接着来更深入的了解数位影像的取样与量化吧! 取样简单来说就是我们要以多少个方格来表示这张图片,方格愈...