Typescript|Generic

Intro

定义

带有类型约束的语言需要泛型(Generic)来实现更加通用、符合标准、可复用的逻辑

在不使用泛型的时候吗,我们确实是可以用 any 作为函数的入参类型约束,但是这个就和 Object 作为入参以及返回值一样,没有类型限制时,外部在调用这段函数时,无法明确确定入参类型,也无法明确确定返回值类型

1
2
3
public Object identity (Object arg) {
return arg;
}
1
2
3
function identity (arg: any) {
return arg;
}

我们可以尝试为这个identity函数引入一个Type变量,这个Type变量和常规变量不同,只作用在类型上而不是js中通常的值上

1
2
3
function identity<Type> (arg: Type): Type {
return arg;
}

针对这个函数的签名

1
2
function identity<Type> (arg: Type): Type 
// 定义类型变量 入参类型 返回值类型

在函数的签名处定义了作用于类型的Type变量,由于此处只有一个类型变量Type,因此这段函数就是将入参类型和返回值类型都和Type变量类型绑定上

调用

在调用泛型函数时,有两种方式:

  • 显式声明类型
1
let foo = identity<string>("foo")
  • 隐式类型推断(更常见)

编译器根据我们传入的参数类型自动为我们设置 类型变量 Type 的值

1
let foo = identity("foo")

这种一般IDE都会在隐式推断计算后(IDE侧)自动将类型标注出来

小总结

我们可以将这样的泛型函数理解为:

这个函数不仅接受一个参数arg,还接受一个类型参数Type

我们在传入参数的时候会自动将变量类型 和 Type 进行绑定

我们在函数中可以使用绑定好的类型参数Type,更大程度上提供了灵活性

泛型函数类型变量

非泛型函数类型变量

在了解泛型函数类型的变量定义之前,我们需要对非泛型的函数类型的变量有一定的了解

例如有这么一个函数

1
2
3
function add(x: number, y: number) {
return x + y;
}

我们想要将这个函数作为一种类型赋值给一个新的变量,这种没有任何泛型出现

1
let addFuncVar: (x: number, y: number) => number = add;

这段表达式比较复杂,需要这么理解:

1
2
let addFuncVar: (x: number, y: number) => number
//变量名 : 类型定义,只不过这个类型是一个函数类型(非泛型)

(x: number, y: number) => number 在 ts 中表示一种函数类型,接收两个 number 作为入参,返回一个 number

简单来说,非泛型函数类型变量的定义格式就是:

let 函数名: (参数类型列表) => 返回值类型 = 实际函数;

泛型函数类型变量

知道了非泛型的,带泛型的和非泛型的很相似

例如有这么一个函数

1
2
3
function identity<Type>(arg: Type): Type {
return arg;
}

带有泛型类型的这个函数类型就应该定义为:

1
let genericFuncVar: <Type>(arg: Type) => Type = identity;

当然这个 类型变量Type 名称是随意的

1
let myIdentity: <Input>(arg: Input) => Input = identity;

函数接口

定义带泛型函数接口

上面的代码<Input>(arg: Input) => Input 就定义了一个函数类型,我们可以将其抽取到一个接口中,这种就是函数接口

1
2
3
interface IdentityFn {
<Input>(arg: Input): Input //注意最后返回值要用冒号而不是箭头
}

后续我们可以直接引用这个函数接口来替代 带有泛型的函数的类型变量

1
let interfaceIdentity: IdentityFn = identity;

将泛型参数提升至函数接口侧

在更多的情况下,我们可能会将泛型的声明定义在接口侧

1
2
3
4
5
interface IdentityFnWithGeneric<Type> {
(arg: Type): Type
}

let interfaceIdentityWithGeneric: IdentityFnWithGeneric<string> = identity;

两种写法总结对比

两种写法,区别在于泛型参数究竟是放在函数侧,还是函数接口侧

直接举个例子更容易理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function identity<Type>(arg: Type): Type {
return arg;
}

interface IdentityFn {
<Input>(arg: Input): Input
}

let interfaceIdentity: IdentityFn = identity;

interfaceIdentity(123) //123可以,泛型会作用于函数

interface IdentityFnWithGeneric<Type> {
(arg: Type): Type
}

let interfaceIdentityWithGeneric: IdentityFnWithGeneric<string> = identity;

interfaceIdentityWithGeneric(123) // 123 错了,必须传入string类型

在上述代码中,一旦我们定义了 let interfaceIdentityWithGeneric: IdentityFnWithGeneric<string> = identity; 则表示这个新的函数类型变量,在调用的时候只能传入 string 类型的参数,返回的也只会是 string 类型,接口内部所有成员都能共享这个类型参数。

通过这种方式,可以让使用者在使用接口时明确地指定类型,更加直观

用法场景 泛型在函数上 <T>(arg: T) => T 泛型在接口上 interface Foo<T>
每次调用可能使用不同类型 ✅ 推荐使用 ❌ 不适合
希望使用时一次性指定固定类型 ❌ 不够清晰 ✅ 推荐使用
接口内有多个成员都要用这个类型 ❌ 不方便 ✅ 类型统一,语义清晰

泛型类

ts中的class更偏oop一些,但是通过泛型类我们可以更好地理解上面的内容中将泛型提升至接口侧带来的便利性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class GenericNumber<NumberType> {
zeroValue: NumberType;
add: (x: NumberType, y: NumberType) => NumberType;
}

let genericNumber = new GenericNumber<number>();
genericNumber.zeroValue = 0;
genericNumber.add = (x, y) => x + y;

let genericString = new GenericNumber<string>();
genericString.zeroValue = "";
genericString.add = (x, y) => x.concat(y);

genericString.add(genericString.zeroValue, "test");

将类型参数放在类本身上,可以确保类的所有属性都使用相同的类型

类型约束

这一部分其实和别的编程语言也差不太多,我们很多时候希望类型参数绑定的类型并不是表示任何类型,而是有一定的约束

例如我们希望在函数内部对类型参数的变量调用指定的length字段

1
2
3
4
5
6
7
8
interface Lengthwise {
length: number;
}

function logLength<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); //确保一定会有 length 属性
return arg;
}

如果此时我们在外部调用的时候传入了一个不符合约束的类型变量,同样也会报错:

1
logLength(1) //TS2345: Argument of type number is not assignable to parameter of type Lengthwise
1
logLength({ length: 1, name: 'foo' }); //√

Typescript|Generic
http://example.com/2025/06/16/Typescript-Generic/
作者
Noctis64
发布于
2025年6月16日
许可协议