一文读懂 TypeScript 中的范型是如何计算的

本文详细介绍了 TypeScript 中的泛型,包括泛型约束、交叉类型、联合类型、条件类型、infer 关键字、映射类型、模板文字类型等,并通过实例展示了它们的用法和应用场景,帮助读者深入理解 TypeScript 的类型系统。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本人首发于五块木头个人博客

泛型

首先, 我们来写一个函数: loggerNum 函数, 这个函数的作用是 console.log 输入数字值, 然后将该值返回:

const loggerNum = (params: number) => {
   
  console.log(params);
  return params;
};
loggerNum(1); // number
loggerNum('1'); // Error: 字符串不能分配给数字类型

假如说我们还需要打印字符串呢?

const loggerStr = (params: string) => {
   
  console.log(params);
  return params;
};
loggerStr('1'); // string

假如说我们还有布尔类型, 害, 是不是得继续声明一个函数来实现, 算了, 我使用 any:

const logger = (params: any) => {
   
  console.log(params);
  return params;
};

// logger(1); // any
// logger('1'); // any
// logger(false); // any

这种方式当然没有问题, 但是它失去了原有的类型以及类型检查(不到万不得已, 请不要使用 any), 所以我们需要一种捕捉参数类型的方式, 以便我们可以使用它来表示返回的内容.

function logger<Type>(params: Type): Type {
   
  console.log(params);
  return params;
}

我们现在给logger函数添加来一个类型变量Type, 然后Type可以捕获到提供的类型(比如说numberstring…), 后面我们就可以使用该Type, 在这里我们使用Type作为参数和返回值, 所以 Ts 会检查其返回值是不是与Type是一致的.

logger<number>(1); // number
logger<string>('1'); // string
logger<boolean>(true); // boolean
logger<number[]>([1, 2, 3]); // number[]
logger<string>(1); // 数字类型不能分配给字符串类型

我们也可以使用类型参数自动推断的方式, ts 编译器会根据我们传入的参数自动设置其类型.

logger(1); // number
logger('1'); // string
logger(true); // boolean
logger([1, 2, 3]); // number[]

我们第一个简单的泛型函数就实现啦~.

泛型约束

我们再拿上面那个例子来说, 我们想打印一下参数的长度, 改一下代码:

function logger<Type>(params: Type): Type {
   
  console.log(params.length); // Property 'length' does not exist on type 'Type'.
  return params;
}

报错了, 别慌张, 因为使用泛型, 我们此时不能访问它的任何属性, 所以这个时候报错是正常的, 做一个类比, 我们把之前的例子比喻成一个充电器, 什么充电器都可以, 只要是充电器就好了, 现在充电器加了约束(类型约束), 只能适配 type-c 充电器, 不是 type-c 充电器都不行.

这里也是一样的, 我们希望参数把类型做一个限制, 至少具有length属性的类型才可以传入, 所以来看看代码.

function logger<Type extends {
      length: number }>(params: Type): Type {
   
  console.log(params.length);
  return params;
}

Type extends {length:number} 就是做了一个类型约束, 只有具有length属性的对象才可以传入.

logger({
    length: 0 });
logger([1, 2, 3]);
logger(1); // number 不能分配给 { length:number }

在泛型约束中使用类型参数

有了上面的基础, 我们再来实现一个方法getProperty(obj, key)返回对象中的指定 key 的 value.

function getProperty<Type>(obj: Type, key: keyof Type) {
   
  return obj[key];
}

keyof 后面会介绍

默认类型参数

在 Js 中有默认参数, 如果没有传值时使用该默认值.

const inc = (count, step = 1) => count + step;
inc(1); // 2
inc(1, 5); // 6

而在 Ts 中有默认类型参数, 如果没有传入参数就使用默认类型参数, 上面getProperty的例子更新一下:

function getProperty<Type, Key = keyof Type>(obj: Type, key: Key) {
   
  return obj[key]; // Key 不能当作Type的索引
}

成功的报错了, Key在不传入类型的时候,才会是默认的keyof Type, 而如果Key如果传入了numberstring等类型时, Key就会采用传入的类型:

getProperty({
    name: 'senlin', age: 18 }, false); // getProperty(obj: Person, key: boolean)
getProperty({
    name: 'senlin', age: 18 }, '444'); // getProperty(obj: Person, key: string)

所以我们需要对Key做一个参数类型约束:

function getProperty<Type, Key extends keyof Type = keyof Type>(
  obj: Type,
  key: Key
) {
   
  return obj[key];
}

getProperty({
    name: 'senlin', age: 18 }, false); // Error: false不能分配给'name'|'age'
getProperty({
    name: 'senlin', age: 18 }, '444'); // Error: '444'不能分配给'name'|'age'
getProperty({
    name: 'senlin', age: 18 }, 'name'); // OK: 'senlin'

交叉类型 &

类型运算符 & 用于创建交叉类型:

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// "b" | "c"
type Intersection = A & B;

如果我们将类型 A 和类型 B 视为集合, 那么 A & B 就是两个集合的交集, 换句来说: 结果的成员是两个操作数的成员.

与 never、unknown 的爱恨情仇

type A = 'a' | 'b';

type D = A & never; // never
type E = A & unknown; // 'a' | 'b'

如果把 ts 的类型当作一个集合来看的话, unknown 相当于集合中的全集, 它是一个顶部类型:

  1. 空集(never)和其他集合(A)做交集(交叉类型) = 空集(never).
  2. 全集(unknown)和其他集合(A)做交集(交叉类型) = 其他类型.

联合类型 |

我们有这样子的一个函数, 接受一个参数, 如果是数组, 则原样返回, 如果不是数组, 将值包裹成数组.

function wrapToArray(params: number) {
   
  return [params];
}

wrapToArray(1); // number[]
wrapToArray([1]); // number[] 不能分配给number

所以这个时候, 我们就需要采用联合类型, 期望参数可以传入数字和数字数组.

function wrapToArray(params: number | number[]) {
   
  // 类型缩小
  if (Array.isArray(params)) return params;
  return [params];
}

wrapToArray(1); // number[]
wrapToArray([1]); // number[]

注意这里的参数number|number[], 意思就是允许传入数字和数字数组类型, 这里我们使用Array.isArray来进行类型缩小, 如果是数组就直接返回, 如果不是数组就进行包裹一层返回.

为什么要进行类型缩小? 因为 Ts 在使用过程中需要明确具体的类型(any 除外!), 当前类型是number|number[], 并不清楚是number类型还是number[]类型, 所以需要使用Array.isArray来将其缩小到number[]类型.

我们也可以使用|来创建联合类型:

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// "a" | "b" | "c" | "d"
type Union = A | B;

如果把 A 和 B 当作两个集合, 联合类型就是求两个集合的并集, 结果的成员是至少一个操作数的成员.

如果我们对一个对象类型进行keyof操作的时候, 也会得到联合类型:

type Person = {
   
  name: string;
  age: number;
};

type PersonKeys = keyof Person; // 'name' | 'age'

对象类型的联合

由于联合类型的每个成员都是至少一个组件类型的成员, 我们只能安全的访问所有组件类型共享的属性(A 行). 如果要访问其他属性, 我们需要一个类型保护(B 行):

type 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值