当前位置: 首页 > news >正文

TypeScript 从零基础到精通(五):高级类型与泛型

摘要:泛型是 TypeScript 最强大、最核心的高级特性之一。它允许我们编写“适用于广泛类型”的代码,而不是为每个类型重复编写逻辑。本文将从泛型的动机讲起,逐步深入到泛型函数、泛型接口、泛型类、泛型约束,再到映射类型、条件类型以及 TS 内置的工具类型(Partial、Required、Pick、Omit、Readonly、Record 等)。


一、前言

在前四篇文章中,我们已经掌握了 TypeScript 的基础类型、函数、接口、类以及面向对象编程。现在我们可以给大部分代码加上类型,让编译器帮我们检查错误。

然而,你会遇到这样的场景:编写一个通用函数,比如“获取数组中第一个元素”。如果是数字数组,返回number;字符串数组,返回string;用户对象数组,返回User。不使用泛型的话,我们只能写多个重载或用any(丢失类型信息)。

// 使用 any 类型不安全 function firstElement(arr: any[]): any { return arr[0]; } ​ const num = firstElement([1, 2, 3]); // num 类型是 any,无法享受后续类型检查

泛型就是解决这个问题的:把“类型”也作为参数,在调用时再确定具体类型。


二、泛型的动机:让“类型参数化”

想象一下,你写了一个函数identity,它返回传入的参数本身。在 JavaScript 中很简单:

function identity(arg) { return arg; }

但在 TypeScript 中,如果要求类型安全,你可能想为每个类型写一个版本:

function identityNumber(arg: number): number { return arg; } function identityString(arg: string): string { return arg; } // 不可能为所有类型都写一遍

泛型允许我们定义一个类型变量(Type Variable),在调用时才填充:

function identity<T>(arg: T): T { return arg; } ​ // 调用时自动推导类型 let output1 = identity("hello"); // 类型为 string let output2 = identity(42); // 类型为 number

<T>表示声明一个类型变量T,它会在函数调用时被具体的类型(如stringnumber)替换。


三、泛型函数

3.1 基本语法与使用

泛型函数在参数列表前使用<T>(可以用任何标识符,通常用TUKV)。

function getArrayLength<T>(arr: T[]): number { return arr.length; } ​ console.log(getArrayLength([1, 2, 3])); // T 被推导为 number console.log(getArrayLength(["a", "b", "c"])); // T 被推导为 string

3.2 类型推导与显式指定

大多数情况下,TypeScript 能根据参数自动推导类型变量。你也可以手动指定:

let result = identity<string>("hello"); // 显式指定 T = string

手动指定在参数不足以推导时很有用(例如没有参数,或类型需要精确控制)。

3.3 多个类型参数

可以同时使用多个类型变量:

function merge<T, U>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 }; } ​ const merged = merge({ name: "Tom" }, { age: 25 }); // merged 类型为 { name: string } & { age: number } => { name: string; age: number } console.log(merged.name, merged.age);


四、泛型接口与泛型类

4.1 泛型接口

接口也可以使用泛型,使其更灵活。

interface Box<T> { value: T; getValue(): T; } ​ const stringBox: Box<string> = { value: "hello", getValue() { return this.value; } }; ​ const numberBox: Box<number> = { value: 100, getValue() { return this.value; } };

泛型接口也常用于定义函数类型:

interface Comparator<T> { (a: T, b: T): number; } const compareNumbers: Comparator<number> = (a, b) => a - b;

4.2 泛型类

类和接口类似,可以在类名后加上<T>

class Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; } } const numberStack = new Stack<number>(); numberStack.push(1); numberStack.push(2); console.log(numberStack.pop()); // 2 (类型为 number | undefined) const stringStack = new Stack<string>(); stringStack.push("a");

静态成员不能引用类的类型参数,因为静态成员属于类本身,而非实例。


五、泛型约束(Constraints)

有时候我们希望类型变量必须满足某些条件(比如必须有length属性)。这时可以使用extends关键字来约束。

5.1 基本约束

interface Lengthwise { length: number; } function logLength<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; } logLength("hello"); // ✅ 字符串有 length logLength([1, 2, 3]); // ✅ 数组有 length // logLength(123); // ❌ 数字没有 length 属性

5.2 使用 keyof 约束属性名

当你需要确保传入的键(key)确实存在于某个对象中时,可以使用keyof操作符。

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const person = { name: "Alice", age: 30 }; const nameValue = getProperty(person, "name"); // string const ageValue = getProperty(person, "age"); // number // const invalid = getProperty(person, "gender"); // ❌ 参数 "gender" 不能赋给 "name" | "age"

keyof T是一个联合类型,包含 T 的所有公共属性名。


六、泛型默认类型

我们可以为泛型参数指定默认类型,类似函数参数的默认值。当调用者不指定时,使用默认类型。

interface ApiResponse<T = any> { code: number; data: T; message: string; } // 使用默认类型 any const res1: ApiResponse = { code: 200, data: "ok", message: "success" }; // 指定具体类型 const res2: ApiResponse<{ id: number }> = { code: 200, data: { id: 1 }, message: "success" };

默认类型在有可选参数或复杂层级时非常有用。


七、映射类型(Mapped Types)

映射类型允许你基于旧类型创建新类型,通过对旧类型的每个属性进行转换。

7.1 基础语法

映射类型的语法是{ [P in K]: T },其中K是一个联合类型(通常是keyof T)。

type Readonly<T> = { readonly [P in keyof T]: T[P]; }; type Partial<T> = { [P in keyof T]?: T[P]; };

其实 TypeScript 内置了这些工具类型(见后文)。我们可以自己实现一个简单的映射类型,把所有属性变成nullundefined

type Nullable<T> = { [P in keyof T]: T[P] | null; }; interface User { id: number; name: string; } type NullableUser = Nullable<User>; // 等价于 { id: number | null; name: string | null; }

7.2 映射修饰符

readonly?是映射类型中的修饰符。我们可以通过前缀+-来添加或移除修饰符(+是默认的)。

// 移除所有属性的 readonly type Mutable<T> = { -readonly [P in keyof T]: T[P]; }; // 移除所有属性的可选修饰符(变成必选) type Required<T> = { [P in keyof T]-?: T[P]; };

八、条件类型(Conditional Types)

条件类型类似于 JavaScript 的三元运算符:T extends U ? X : Y。它根据类型关系选择不同的类型。

8.1 基本语法

type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<number>; // false

8.2 分布式条件类型

当条件类型作用于泛型且该泛型是联合类型时,TS 会将联合类型的每个成员分别代入条件,最后合并结果。这称为分布式条件类型。

type ToArray<T> = T extends any ? T[] : never; type Result = ToArray<string | number>; // 等价于 (string extends any ? string[] : never) | (number extends any ? number[] : never) // 结果: string[] | number[]

防止分布式:用方括号包裹[T]

type ToArrayNonDist<T> = [T] extends [any] ? T[] : never; type Result2 = ToArrayNonDist<string | number>; // (string | number)[]

8.3 infer 关键字

infer允许我们在条件类型中声明一个待推断的类型变量,常用于提取类型的内部结构。

// 获取函数返回值类型 type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function foo(): boolean { return true; } type FooReturn = ReturnType<typeof foo>; // boolean // 获取数组元素类型 type ElementType<T> = T extends (infer U)[] ? U : T; type E1 = ElementType<number[]>; // number type E2 = ElementType<string>; // string (不变)

infer也可以用于元组和 Promise 等。


九、内置工具类型详解

TypeScript 内置了许多常用的类型工具,极大提升了开发效率。下面逐一介绍。

9.1Partial<T>—— 所有属性变为可选

interface Todo { title: string; description: string; completed: boolean; } function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>): Todo { return { ...todo, ...fieldsToUpdate }; } const todo1: Todo = { title: "Learn TS", description: "Study", completed: false }; const todo2 = updateTodo(todo1, { description: "Study advanced" });

9.2Required<T>—— 所有属性变为必选

interface Props { a?: number; b?: string; } const obj: Required<Props> = { a: 5, b: "hello" }; // 必须提供 a 和 b

9.3Readonly<T>—— 所有属性变为只读

const frozen: Readonly<Todo> = { title: "Freeze", description: "Immutable", completed: false }; // frozen.title = "Changed"; // ❌

9.4Pick<T, K>—— 从 T 中挑选部分属性

type TodoPreview = Pick<Todo, "title" | "completed">; // { title: string; completed: boolean; }

9.5Omit<T, K>—— 从 T 中排除部分属性

type TodoInfo = Omit<Todo, "completed">; // { title: string; description: string; }

9.6Record<K, T>—— 构造一个对象类型,键为 K,值为 T

type PageInfo = { title: string; url: string; }; type Page = "home" | "about" | "contact"; const pages: Record<Page, PageInfo> = { home: { title: "Home", url: "/" }, about: { title: "About", url: "/about" }, contact: { title: "Contact", url: "/contact" } };

9.7Exclude<T, U>—— 从 T 中排除可赋值给 U 的类型

type T = Exclude<"a" | "b" | "c", "a" | "b">; // "c"

9.8Extract<T, U>—— 提取 T 中可赋值给 U 的类型

type T = Extract<"a" | "b" | "c", "a" | "d">; // "a"

9.9NonNullable<T>—— 排除 null 和 undefined

type T = NonNullable<string | number | null | undefined>; // string | number

9.10ReturnType<T>—— 获取函数返回值类型

function getString(): string { return "hello"; } type R = ReturnType<typeof getString>; // string

9.11Parameters<T>—— 获取函数参数类型(元组)

function greet(name: string, age: number): void {} type Params = Parameters<typeof greet>; // [string, number]

十、总结

本文深入讲解了 TypeScript 的高级类型特性:

泛型

  • 让类型变量化,编写可复用的组件

  • 泛型函数、泛型接口、泛型类

  • 泛型约束(extends+keyof

  • 泛型默认类型

映射类型

  • 基于旧类型通过[P in keyof T]生成新类型

  • 修饰符readonly?及加减操作

条件类型

  • T extends U ? X : Y

  • 分布式条件类型(联合类型自动分发)

  • infer提取类型

内置工具类型

  • PartialRequiredReadonlyPickOmitRecordExcludeExtractNonNullableReturnTypeParameters

这些高级特性是 TypeScript 区别于普通类型检查器的核心优势,也是写出健壮、灵活、可维护代码的关键。


如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

http://www.cnnetsun.cn/news/2799585.html

相关文章:

  • 修改带mermaid的html文件生成bug:国产模型束手
  • 别只盯着热点函数了!用Intel VTune的‘异常探测’和‘内存消耗’分析揪出隐藏的性能鬼影
  • RAG系统性能优化与视觉分析方法实践
  • SAP BASIS入门实操:手把手教你配置STMS传输请求(从清空到测试全流程)
  • 为什么你的专栏引流失效?CSDN后台最新V2.3.8算法升级后,必须重配的6个AI链接关键字段
  • 云计算从入门到云原生:一篇文章吃透虚拟化、容器化、IaC与编排
  • 告别网络卡顿:手把手教你为RoCEv2配置DC-QCN拥塞控制(附mlnx_qcn命令详解)
  • 技术博客冷启动秘籍:巧用emoji提升CSDN文章打开率与互动数据
  • 独家拆解CSDN AI引流系统架构:仅限认证技术博主开放的「专栏级LinkID」动态绑定机制(内测资格倒计时72小时)
  • 从收音机到5G滤波器:品质因数Q如何影响你的手机信号和网速?
  • 【紧急预警】CSDN AI营销导流规则即将动态收紧!现在不掌握这4个合规导流杠杆,下月起私信触达率或腰斩
  • Spring AI Alibaba向量存储:5种企业级架构方案深度对比
  • 行政区划 ZIP 导入(importZip)
  • BilibiliDown终极指南:三分钟掌握B站视频下载神器
  • 8类果树病害检测数据集(炭疽病/白粉病/根腐病等)| 6000张YOLO智慧农业病虫害监测数据集 适用于果园智能监测、病害识别与目标检测研究
  • 怎么监控对标账号更新,5款作者监控工具横评实测
  • G-Helper终极指南:如何让华硕笔记本性能翻倍的轻量级控制工具
  • K210人脸识别门禁实战:如何用MaixPy实现口罩检测与特征学习
  • 从dBi到隔离度:一文读懂天线数据手册里的那些‘黑话’,让你的产品射频性能不再玄学
  • 用Python和PuLP搞定选址问题:从消防站到外卖配送点的实战建模指南
  • MATLAB旁瓣分析工具集:一键计算雷达波形PSLR与ISLR
  • 终极指南:如何用Warcraft Helper彻底修复魔兽争霸3在Win10/Win11的兼容性问题
  • 基于STM32的智能抽水装置设计:从传感器融合到电机驱动的完整实现
  • 北京出租车GPS轨迹分析包:2014年单日数据+上下车热点自动识别+交互地图一键生成
  • 大模型与深度学习确定性控制:基于 PyTorch 的随机种子(Seed)全局锚定与 CUDA 算子确定性配置规避精度抖动实战
  • ABot-Claw——改进OpenClaw以驱动双足机器人自主干活的三个关键点:统一具身接口、视觉多模态记忆、基于奖励模型的执行反馈模块
  • Forza Mods AIO终极指南:3分钟掌握免费开源游戏修改工具
  • 151.高通深度救砖脚本|9008 EDL模式Sahara协议传输,黑砖设备强制恢复
  • 编程教育的新篇章:AI工具如何改变教学方式
  • 基于IEEE14节点的电力系统碳流追踪MATLAB仿真包(含潮流计算与碳责任分配核心函数)