文章目录
- TypeScript
-
- 什么是TypeScript
- [TypeScript 的执行机制](#TypeScript 的执行机制)
- 背后的原理
- 实际开发中的演进
- [区分 TS 和 JS](#区分 TS 和 JS)
- [TypeScript 编译器 ------ tsc](#TypeScript 编译器 —— tsc)
- 基础类型
-
- [原始类型: string, number, boolean](#原始类型: string, number, boolean)
- [数组: type[] 或 Array<type>](#数组: type[] 或 Array<type>)
- [元组: [string, number]](#元组: [string, number])
- [any / unknown / void / null / undefined / never](#any / unknown / void / null / undefined / never)
- [联合类型 (|) , 字面量类型 ,交叉类型 (&)](#联合类型 (|) , 字面量类型 ,交叉类型 (&))
- 类型注解与推断
- 函数
-
- 参数类型与返回类型
- [可选参数 , 默认参数, 剩余参数](#可选参数 , 默认参数, 剩余参数)
- 函数重载 (overload signatures)
- [泛型函数 <T>](#泛型函数 <T>)
- 接口与类型别名
-
- interface (接口)
- type(类型别名)
- [接口继承 vs 类型交织](#接口继承 vs 类型交织)
- 注意事项
- 实际项目中的选择策略
- 类
-
- 属性与构造器 (public/private/protected)
- [特殊修饰符详解 readonly, static, abstract](#特殊修饰符详解 readonly, static, abstract)
- 实现接口 (implements)
- 参数属性
- 泛型
-
- 函数泛型
- 接口泛型
- 类型别名泛型
- 类泛型
- [泛型约束 extends](#泛型约束 extends)
- 多个类型参数
- 默认泛型参数
- 泛型参数的作用域与嵌套
- 泛型参数用于高阶类型
- [typeof 与泛型的混合语法](#typeof 与泛型的混合语法)
- [在 TSX/React 组件中](#在 TSX/React 组件中)
- 泛型参数的命名惯例(非强制但常见)
- 语法要点总结
- 高级类型
-
- 类型守卫
- 映射类型
- 条件类型
- 条件类型的基本概念
- 分布式条件类型
- [infer 关键字:在条件类型中推断类型](#infer 关键字:在条件类型中推断类型)
- [工具类型(Utility Types)](#工具类型(Utility Types))
-
- [Exclude<T, U>](#Exclude<T, U>)
- [Extract<T, U>](#Extract<T, U>)
- NonNullable<T>
- ReturnType<T>
- Parameters<T>
- InstanceType<T>
- ConstructorParameters<T>
- 注意事项
- 索引访问类型 (T[K])
TypeScript
什么是TypeScript
TypeScript 是一个为JavaScript添加了静态类型检查的,可编译为纯JavaScript的预处理器。
- 它是一门语言:拥有自己的语法(类型注解、接口、泛型等)。这些语法在标准 JavaScript 中是不存在的。
- 它是 JavaScript 的超集:所有合法的 JavaScript 代码,都是合法的 TypeScript 代码。这意味着你可以在任何 .js 文件里,直接把它重命名为 .ts,然后立刻获得类型检查的能力。
- 它最终会消失:TypeScript 编译器(tsc)会把所有的类型注解、接口、泛型等 TS 独有的语法"擦除",只生成纯净、标准的 JavaScript 代码。浏览器只能运行 JavaScript,它永远不认识 TypeScript。
TypeScript 的执行机制
TypeScript 本身无法直接执行,它会先被编译器(tsc)编译成 JavaScript,然后由 JS 引擎(如 V8、Node.js)来执行。
这是一个 "编译时 (Compile-time) " 和 "运行时 (Runtime)" 完全分离的机制。
- 编写阶段:你写的是 .ts 文件,里面有类型。
- 编译阶段 :运行 tsc 命令。tsc 会:
- 解析你的 .ts 文件。
- 进行全面的类型检查,如果发现类型错误,会报错并停止编译。
- 擦除所有类型代码,生成纯 .js 文件。
- 执行阶段:运行 node my-file.js (或者用
以下面 greeter.ts 文件为例
ts
// TypeScript 源码
function greet(name: string): string {
return `Hello, ${name}!`;
}
const userName: string = "World";
console.log(greet(userName));
第 1 步:类型检查与编译
在终端运行 tsc greeter.ts(或者构建工具里的 tsc)。编译器会做两件事:
- 检查:确认 name 和 userName 确实是 string 类型。
- 编译:生成 greeter.js 文件,内容如下:
js
// 这是编译后的纯 JavaScript 代码
function greet(name) {
return `Hello, ${name}!`;
}
var userName = "World";
console.log(greet(userName));
在编译后的js文件里面,所有类型信息(: string)都被完全擦除了。这就是 TS 在这个阶段做的最核心的工作。
第 2 步:执行
在浏览器里通过 加载它,或者在 Node 里运行 node greeter.js。JS 引擎将执行这个 .js 文件,输出 Hello, World!。JS 引擎自始至终没见过任何 TS 特有的语法。
背后的原理
- 编译时 vs 运行时
- 编译时:发生在你的电脑上(你开发时)。这是 TypeScript 的战场,它负责检查所有类型错误。
- 运行时:发生在用户的浏览器或服务器上(代码部署后)。这是 JavaScript 的战场,TS 的类型信息在此时已不存在。
- 核心价值 :TS 把所有可能出错的时刻,从昂贵的"运行时"提前到了便宜的"编译时"。早发现错误,修复成本越低。
- 类型擦除 (Type Erasure)
这是最关键的技术。TS 的编译器在工作时,会遍历整个语法树,识别出哪些是 JS 的标准语法,哪些是 TS 独有的类型注解。对于类型注解,它只会把它们当作标记来检查,而不会翻译成任何 JS 代码,直接丢弃(擦除)。这就是为什么最终的 .js 文件里没有任何类型信息。 - AOT 编译 (Ahead-Of-Time)
TS 是一种"提前编译"的语言,类似于 C++ 或 Rust(它们编译成机器码),而不是像 Python 或 JS 那样"解释执行"。你始终在运行 tsc 生成的代码,而不是原始代码。
实际开发中的演进
在现代前端工程中,几乎不会直接调用 tsc 命令。因为那太慢了,而且功能单一。通常会配合使用:
- 构建工具 :Webpack(配合 ts-loader)、Vite、esbuild、SWC 等工具会接管 TS 文件的编译。它们通常会跳过类型检查(为了极致速度),只做"代码转换"(擦除类型、降级语法)。
- 独立的类型检查 :在 package.json 里写一个 "type-check": "tsc --noEmit" 命令。
- --noEmit 的意思是"只做类型检查,不要生成 JS 文件"。
- 在开发时,一边用 Vite 这样的工具快速编译并热更新 (不检查类型),另一边可以在 IDE 里或单独运行 npm run type-check 来异步检查类型。这样既保证了开发体验的速度,又获得了类型安全。
区分 TS 和 JS
| 特性 | TypeScript (TS) | JavaScript (JS) |
|---|---|---|
| 本质 | 编程语言 + 静态类型检查器 | 编程语言 |
| 能被浏览器/Node直接执行吗 | 不能 | 能 |
| 执行过程 | 先编译,得到 JS,再执行这个 JS | 直接解释执行 |
| 类型信息 | 编写时存在,编译时擦除 | 不存在 |
| 错误发现时机 | 编译时 (开发阶段) | 运行时 (可能已经部署) |
| 开发工具支持 | 极强的自动补全、重构、导航 | 较弱或没有类型推断 |
JavaScript 只提供了动态类型 ------ 执行代码,然后才能知道会发生什么事。
TypeScript 使用一个静态的类型系统,在代码实际执行前预测代码的行为。
TypeScript 编译器 ------ tsc
新建一个文件夹ts,在此文件夹进行安装
bash
npm install -g typescript
在此文件夹ts下新建一个文件,hello.ts
js
console.log('Hello world!');
在ts文件终端运行 typescript 安装包自带的 tsc 指令进行类型检查。
bash
tsc hello.ts
在文件夹下能看到多出了 hello.js 文件,这是 tsc 编译或者转换 hello.ts 文件之后输出的纯 JavaScript 文件。
基础类型
原始类型: string, number, boolean
类型注解(Type Annotation ):在 TypeScript 中,使用 变量名: 类型 的语法来为变量、函数参数、函数返回值等添加类型标注。
核心语法:
js
let/const/var 变量名: 类型 = 值;
- string - 字符串类型
let 变量名: string = '值';
let 变量名: string =
模板字符串 ${表达式};
ts
// 基本声明
let firstName: string = "张";
let lastName: string = '三';
let fullName: string = `${firstName}${lastName}`; // "张三"
// 字符串操作
let message: string = "Hello TypeScript";
console.log(message.length); // 16
console.log(message.toUpperCase()); // "HELLO TYPESCRIPT"
console.log(message.toLowerCase()); // "hello typescript"
console.log(message.substring(0, 5)); // "Hello"
console.log(message.includes("Type")); // true
console.log(message.replace("Type", "Java")); // "Hello JavaScript"
- number - 数字类型
let 变量名: number = 数字;
ts
// 整数和浮点数
let age: number = 25;
let price: number = 99.99;
let temperature: number = -5.5;
let timestamp: number = Date.now();
// 不同进制表示
let decimal: number = 42; // 十进制
let binary: number = 0b101010; // 二进制 (42)
let octal: number = 0o52; // 八进制 (42)
let hex: number = 0x2A; // 十六进制 (42)
// 科学计数法
let largeNumber: number = 1.5e6; // 1500000
let smallNumber: number = 2.5e-4; // 0.00025
// 特殊值
let infinity: number = Infinity;
let negativeInfinity: number = -Infinity;
let notANumber: number = NaN;
// 数学运算
let sum: number = 10 + 20;
let remainder: number = 17 % 5; // 2
- boolean - 布尔类型
let 变量名: boolean = true;
let 变量名: boolean = false;
js
// 基本声明
let isActive: boolean = true;
let isCompleted: boolean = false;
let isLoggedIn: boolean = checkAuthStatus(); // 函数返回 boolean
// 表达式计算结果, 比较表达式自动推断为 boolean,但可以显式标注
let age: number = 18;
let isAdult: boolean = age >= 18; // true
let hasPermission: boolean = user.role === "admin";
// 逻辑运算
let isWeekend: boolean = true;
let isHoliday: boolean = false;
let canSleepIn: boolean = isWeekend || isHoliday; // true
let needWork: boolean = !canSleepIn; // false
数组: type[] 或 Array
方式一:类型[](推荐)
let 变量名: 类型[] = [值1, 值2, 值3];
方式二:Array<类型>
let 变量名: Array<类型> = [值1, 值2, 值3];
只读数组
let 变量名: readonly 类型[] = [值1, 值2];
js
// 方式一:类型[]
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];
let flags: boolean[] = [true, false, true];
// 方式二:Array<类型>
let scores: Array<number> = [98, 87, 92];
let fruits: Array<string> = ["apple", "banana"];
// 只读数组
let readonlyList: readonly number[] = [1, 2, 3];
// readonlyList.push(4); // 错误
元组: [string, number]
在 TypeScript 中,元组(Tuple)是一种特殊的数组类型,允许你定义数组中每个元素的类型,且元素的数量固定。数组的值应该和声明的类型一致,且长度一致
每个值应该与前面的类型相对应
let 变量名: [类型1, 类型2, 类型3, ...] = [值1, 值2, 值3];
可选元素
值2后面加问号代表值2可以省略
let 变量名: [类型1, 类型2?] = [值1];
剩余元素
值3 4 5都属于剩余元素,类型为类型3[]
let 变量名: [类型1, 类型2, ...类型3[]] = [值1, 值2, 值3, 值4, 值5]
只读元组,不能修改值
let 变量名: readonly [类型1, 类型2] = [值1, 值2];
js
// 基本元组
let tuple: [string, number, boolean] = ['hello', 42, true]; // 正确;类型和长度匹配
let tuple: [string, number, boolean] = [42, 'hello', true]; // 错误:类型顺序不匹配
let tuple: [string, number, boolean] =['hello', 42]; // 错误:缺少元素
// 可选元素
let tuple: [string, number?] = ['hello']; // 可选元素可以省略
let tuple: [string, number?] = ['hello', 42];
// 剩余元素
let arr: [string, number, ...boolean[]]= ['hello', 42, true, false, true];
// 只读元组
const point: readonly [number, number] = [10, 20];
any / unknown / void / null / undefined / never
- any - 任意类型(谨慎使用)
绕过 TypeScript 的类型检查,用于处理动态内容或迁移旧代码
let 变量名: any = 任意值;
ts
// any 类型可以接受任何值
let dynamicValue: any = 42;
dynamicValue = "字符串";
dynamicValue = [1, 2, 3];
// any 类型可以调用任何方法(危险的)
let something: any = "文本";
something.toUpperCase(); // 编译通过
something.push(123); // 编译通过(但运行时出错)
something.nonExistentMethod(); // 编译通过(运行时可能崩溃)
// any 可以赋值给任何类型
let anyValue: any = "hello";
let str: string = anyValue; // 可以
let num: number = anyValue; // 可以(危险!)
// 实际应用场景(谨慎使用)
// 1. 处理第三方 API 返回的不确定数据
let apiResponse: any = await fetch("https://api.example.com/data").then(r => r.json());
// 2. 迁移 JavaScript 项目(临时使用)
let existingCodeVariable: any = getFromOldJSFunction();
// 3. 动态配置对象
let config: any = {};
config.anyProperty = "任意值";
config.dynamicMethod = () => console.log("动态方法");
- unknown
表示任何值,但比 any 更严格,强制进行类型检查。
let 变量名: unknown = 任意值;
ts
// unknown 可以接收任何类型的值
let safer: unknown = "something";
// safer.toUpperCase(); // 错误,不能直接调用
if (typeof safer === "string") {
safer.toUpperCase(); // 类型检查后可以
}
- void - 无返回值类型
表示函数不返回任何有意义的值,或者变量值为 undefined
function 函数名(): void { }
let 变量名: void = undefined;
js
// 不返回值的函数
function logMessage(message: string): void {
console.log(`[LOG] ${message}`);
// 没有 return 语句
}
function showAlert(message: string): void {
alert(message);
return; // 允许空的 return 语句
}
// 异步函数返回 void
async function fetchData(): Promise<void> {
await someAsyncOperation();
console.log("数据获取完成");
}
// 事件处理器
let button = document.querySelector("button");
if (button) {
button.onclick = (event: MouseEvent): void => {
console.log("按钮被点击", event);
};
}
- null 和 undefined
null 和 undefined 本身是类型
ts
let u: undefined = undefined; // 只能赋值 undefined
let n: null = null; // 只能赋值 null
// 默认情况下,它们是所有类型的子类型(strictNullChecks: false)
let str: string = null; // 只在关闭严格空检查时有效
let num: number = undefined;
// 推荐开启严格模式
// tsconfig.json: "strictNullChecks": true
let str: string = null; // 错误
let str: string | null = null; // 正确,使用联合类型
let str: string | undefined = undefined;
- never - 永不存在的值
ts
// 抛出异常或无限循环的函数
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
联合类型 (|) , 字面量类型 ,交叉类型 (&)
- 联合类型 (|)
联合类型表示值可以是多种类型中的一种,使用竖线 | 分隔。就是或,多选一
ts
// 变量可以是 string 或 number 类型
let id: string | number;
id = 'abc123';
id = 456;
// id = true; // 错误
- 字面量类型
字面量类型是指将具体的值作为类型使用,包括字符串、数字、布尔值等。
ts
// 字符串字面量类型
let direction: 'left' | 'right' | 'up' | 'down';
direction = 'left'; // ✅ 正确
direction = 'right'; // ✅ 正确
// direction = 'top'; // ❌ 错误,只能是 'left'|'right'|'up'|'down'
// 数字字面量类型
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 3; // ✅ 正确
// dice = 7; // ❌ 错误
// 布尔字面量类型
let success: true;
success = true; // ✅ 正确
// success = false; // ❌ 错误,只能是 true
- 交叉类型 (&)
交叉类型将多个类型合并为一个类型,包含所有类型的特性。
接口和类型后面会讲到,看不懂可以先跳过。后面会再讲
ts
// 合并两个接口
interface Person {
name: string;
age: number;
}
interface Employee {
company: string;
salary: number;
}
// 交叉类型:同时拥有 Person 和 Employee 的所有属性
type Staff = Person & Employee;
const staff: Staff = {
name: '张三',
age: 30,
company: '腾讯',
salary: 30000
};
// 合并多个类型
type Colorful = {
color: string;
};
type Circle = {
radius: number;
};
type ColoredCircle = Colorful & Circle;
const redCircle: ColoredCircle = {
color: 'red',
radius: 10
};
类型注解与推断
显式注解 (: type)
类型注解(Type Annotation ):在 TypeScript 中,使用 变量名: 类型 的语法来为变量、函数参数、函数返回值等添加类型标注。就是上面的ts语法
类型推断 (auto)
TypeScript 能根据初始化值自动推断类型,无需显式注解。
ts
// 自动推断为 string
let message = "Hello"; // 等同于: let message: string
// 自动推断为 number
let count = 42; // 等同于: let count: number
// 自动推断为 boolean
let isValid = true; // 等同于: let isValid: boolean
// 推断为 (number | string)[]
let arr = [1, "hello", true]; // 推断为 (number | string | boolean)[]
类型断言 (as 或 <>)
类型断言(Type Assertion)是告诉 TypeScript 编译器"相信我,我知道这个值的类型"的方式。它不会改变值的运行时类型,只是在编译阶段影响类型检查。
- as 语法 (推荐)
值 as 类型
ts
// someValue 被声明为 unknown 类型(可以是任何值,但使用前必须明确类型)
let someValue: unknown = "TypeScript";
// 我们知道它实际存储的是字符串 "TypeScript",但我们想调用字符串的 .length 属性
// console.log(someValue.length); 报错,Object is of type 'unknown' someValue是unknown类型。
// 告诉 TS:"虽然 someValue 是 unknown,但我保证它是 string"
let strLength: number = (someValue as string).length;
- 尖括号 <> 语法
<类型>值
ts
// 传统形式
let someValue: unknown = "TypeScript";
let strLength: number = (<string>someValue).length;
// ⚠️ 注意:在 TSX 中会与 JSX 语法冲突
// 以下代码在 .tsx 文件中会报错
const element = <string>someValue; // 会被解析为 JSX 标签
场景
ts
// 场景:从 localStorage 读取数据
const rawData: unknown = localStorage.getItem('user');
// 我们知道存的是字符串,但 TS 不知道
const userName = (rawData as string).toUpperCase();
// 场景:处理 API 响应
const response: unknown = await fetch('/api/data');
// 我们知道返回的是 JSON 对象
const data = (response as { id: number }).id;
// 或者分开写(推荐)
const typedResponse = response as { id: number };
const data = typedResponse.id;
// 场景:处理事件对象
const button = document.querySelector('button');
button?.addEventListener('click', (event) => {
const target = event.target as HTMLButtonElement;
console.log(target.innerText);
});
// 场景:处理 unknown 类型
function processInput(input: unknown) {
if (typeof input === 'string') {
// 已经通过类型守卫,但有些情况需要断言
const upper = (input as string).toUpperCase();
}
}
选型建议:统一使用 as 语法,兼容性更好。
函数
参数类型与返回类型
1. 语法
ts
function 函数名(参数名: 参数类型): 返回类型 {
// 函数体
return 返回值;
}
2. 例子
ts
// 函数
function sayHello(name: string): string {
return "hello, " + name;
}
// 调用,参数为字符串
sayHello("小明"); // 返回 "hello, 小明"
// 错误使用
sayHello(123); // 错误:参数类型不匹配
sayHello(true); // 错误:参数类型不匹配
解释:
name: string表示这个函数必须接收一个字符串类型的参数:string表示这个函数会返回一个字符串- 如果调用时传入非字符串,TypeScript 会在你写代码时就提示错误
3. 常见的返回值基础类型
ts
// 返回值为字符串类型
function getUserName(id: number): string {
return "用户" + id;
}
// 返回值为数字类型
function doubleNumber(x: number): number {
return x * 2;
}
// 返回值为布尔类型
function isAdult(age: number): boolean {
return age >= 18;
}
// 无返回值(void 类型)
function printMessage(msg: string): void {
console.log(msg);
// 没有 return 语句,或者 return;(不返回任何值)
}
注意 :void 表示函数"不返回任何有用的值"。它可能会执行一些操作(比如打印、保存数据),但不会给你返回结果。
4. 类型推断
ts
// 不需要写返回类型,TypeScript 会自动推断出返回类型是 number
function add(a: number, b: number) {
return a + b; // TypeScript 知道加法结果是数字
}
// 这等同于
function add(a: number, b: number): number {
return a + b;
}
建议:简单的函数可以省略返回类型,但复杂的函数最好明确写出来,让代码更清晰。
5. 箭头函数的类型写法
箭头函数是 ES6 引入的简洁写法,TypeScript 同样支持类型注解:
ts
// 传统函数写法
function multiply(x: number, y: number): number {
return x * y;
}
// 箭头函数写法
const multiply = (x: number, y: number): number => {
return x * y;
};
// 更简洁的箭头函数(单行)
const multiply = (x: number, y: number): number => x * y;
可选参数 , 默认参数, 剩余参数
可选参数(?)
场景:有些参数不是必须的,调用者可以选择是否提供。
ts
// 用问号 ? 表示参数是可选的
function introduce(name: string, age?: number): string {
if (age) {
return `我叫${name},今年${age}岁`;
} else {
return `我叫${name}`;
}
}
// 使用方式
introduce("小明"); // ✅ 输出:"我叫小明"
introduce("小明", 18); // ✅ 输出:"我叫小明,今年18岁"
注:可选参数必须放在必选参数后面
下面这种写法是错误的:
ts
// 可选参数不能出现在必选参数前面
function wrong(age?: number, name: string) { }
默认参数
场景:如果调用者没有提供参数,就使用一个默认值。
ts
// 直接在参数后面用等号赋值
function greet(name: string, greeting: string = "你好"): string {
return `${greeting},${name}`;
}
// 使用方式
greet("小明"); // ✅ 输出:"你好,小明"(使用了默认值)
greet("小明", "早上好"); // ✅ 输出:"早上好,小明"(覆盖默认值)
默认参数和可选参数的区别:
- 可选参数:不提供就是 undefined
- 默认参数:不提供就使用你设定的值
ts
// 可选参数 vs 默认参数
function test1(name?: string): void {
console.log(name); // 不传参时输出 undefined
}
function test2(name: string = "匿名"): void {
console.log(name); // 不传参时输出 "匿名"
}
test1(); // undefined
test2(); // "匿名"
剩余参数(...)
场景:函数需要接收任意数量的参数。
ts
// 使用 ... 把所有剩余参数收集到一个数组中
// 剩余参数必须放在最后
function sumAll(base: number, ...numbers: number[]): number {
// numbers 是一个数组,包含所有额外的参数
let total = base;
for (let num of numbers) {
total += num;
}
return total;
}
// 使用方式
sumAll(10); // 10
sumAll(10, 20); // 30
sumAll(10, 20, 30); // 60
sumAll(10, 20, 30, 40, 50); // 150
解释:
...numbers会把所有传入的额外参数收集起来numbers的类型是number[](数字数组)- 剩余参数必须放在最后
找出最大值
ts
function findMax(first: number, ...rest: number[]): number {
let max = first;
for (let num of rest) {
if (num > max) {
max = num;
}
}
return max;
}
console.log(findMax(5)); // 5
console.log(findMax(5, 10, 3)); // 10
console.log(findMax(1, 2, 3, 4)); // 4
函数重载 (overload signatures)
什么是函数重载?为什么要用重载?
**函数重载:**允许你为同一个函数定义多个不同的类型签名,根据不同的参数类型或数量返回不同的结果。
问题场景:有时候一个函数需要根据传入参数的类型或数量,返回不同类型的结果。
ts
// 这个函数想要实现:
// - 传入一个数字,返回一个数字
// - 传入一个字符串,返回一个字符串
// - 传入一个数组,返回一个数组
function process(value) {
// 根据 value 的类型执行不同的逻辑
}
重载的写法
重载分为两部分:
- 重载签名(多个):告诉 TypeScript 这个函数可以怎么调用
- 实现签名(一个):真正写函数的逻辑
ts
// 步骤1:写重载签名(声明各种可能的调用方式)
function process(value: string): string;
function process(value: number): number;
function process(value: boolean): boolean;
// 步骤2:写实现签名(真正实现)
// 参数和返回值的类型用 联合类型 "|" 来供用户调用时选择哪一种类型
function process(value: string | number | boolean): string | number | boolean {
if (typeof value === "string") {
return value.toUpperCase(); // 字符串转大写
} else if (typeof value === "number") {
return value * 2; // 数字乘以2
} else {
return !value; // 布尔值取反
}
}
// 使用效果
let result1 = process("hello"); // result1 的类型是 string,值是 "HELLO"
let result2 = process(42); // result2 的类型是 number,值是 84
let result3 = process(true); // result3 的类型是 boolean,值是 false
为什么不用联合类型?
ts
// 如果这样写(没有重载)
function process(value: string | number | boolean): string | number | boolean {
// 实现相同...
}
let result = process("hello");
// result 的类型是 string | number | boolean(联合类型)
// TypeScript 不确定具体是哪个,你需要手动判断
result.toUpperCase(); // 报错!TypeScript 认为可能不是字符串
使用重载后,TypeScript 能准确知道返回类型。
参数数量不同的重载
ts
// 重载签名
function getMessage(): string;
function getMessage(name: string): string;
function getMessage(name: string, age: number): string;
// 实现签名
function getMessage(name?: string, age?: number): string {
if (name && age) {
return `我叫${name},今年${age}岁`;
} else if (name) {
return `我叫${name}`;
} else {
return "匿名用户";
}
}
// 使用方式
getMessage(); // "匿名用户"
getMessage("小明"); // "我叫小明"
getMessage("小明", 18); // "我叫小明,今年18岁"
泛型函数
为什么需要泛型?
问题场景 :你想写一个"通用的"函数,可以处理多种类型,但又要保持类型安全。
不使用泛型的困境:
ts
// 方案1:使用 any(失去了类型检查)
function identity(value: any): any {
return value;
}
let result = identity("hello");
result.toUpperCase(); // 可以运行,但 TypeScript 不会保护你
result.toFixed(2); // 运行时错误!字符串没有 toFixed 方法
// 方案2:使用联合类型(不够灵活)
function identity(value: string | number): string | number {
return value;
}
// 输入字符串,返回的可能是数字,类型信息丢失了
泛型的基本用法
泛型就像是"类型的变量",用尖括号 表示:
ts
// T 是一个"类型占位符",代表调用时传入的类型
// <T>:就像是声明一个"类型变量"
// value: T:表示参数的类型就是这个变量
// : T:表示返回值的类型也是这个变量
function identity<T>(value: T): T {
return value;
}
// 调用时,T 被实际类型替换,TypeScript 会根据你传入的值自动确定 T 是什么类型
// 使用方式1:显式指定类型
let result1 = identity<string>("hello"); // result1 的类型是 string
// 使用方式2:让 TypeScript 自动推断(更常用)
let result2 = identity("hello"); // result2 的类型是 string
let result3 = identity(42); // result3 的类型是 number
let result4 = identity(true); // result4 的类型是 boolean
多个泛型参数
ts
// 接收两个不同类型的参数,返回一个数组(元组)
// <T, U>:声明两个类型参数
// first: T:第一个参数类型为 T
// second: U:第二个参数类型为 U
// [T, U]:返回一个元组,第一个元素类型 T,第二个类型 U
function makePair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const pair1 = makePair("name", 18); // 类型: [string, number]
const pair2 = makePair(true, "hello"); // 类型: [boolean, string]
const pair3 = makePair(42, false); // 类型: [number, boolean]
// 可以解构使用
const [name, age] = makePair("小明", 18);
// name 的类型是 string,age 的类型是 number
泛型约束(限制泛型的范围)
问题:有时候你希望泛型必须有某些属性。
ts
// 没有约束的版本
function getLength<T>(value: T): number {
return value.length; // 错误!不是所有类型都有 length 属性
}
// 添加约束:T 必须有 length 属性
// 后面会讲interface,先看看大概怎么写
interface HasLength {
length: number;
}
// extends 关键字的意思是"必须满足这个条件"或"必须是这个形状"。
function getLength<T extends HasLength>(value: T): number {
return value.length; // 正确,因为 T 一定有 length
}
// 使用
getLength("hello"); // 字符串有 length,返回 5
getLength([1, 2, 3]); // 数组有 length,返回 3
getLength({ length: 10 }); // 自定义对象有 length,返回 10
// getLength(123); // 报错:数字没有 length 属性
实际的泛型应用例子
- 获取数组中第一个元素
ts
// <T>:声明一个类型变量,相当于一个"类型占位符"
// arr: T[]:参数是一个数组,数组元素的类型是 T
// : T | undefined:返回值可能是类型 T 或 undefined
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
const firstNumber = getFirst([1, 2, 3]); // 类型:number | undefined
const firstString = getFirst(["a", "b"]); // 类型:string | undefined
const firstBoolean = getFirst([true]); // 类型:boolean | undefined
const empty = getFirst([]); // 类型: undefined
- 类型安全的 API 请求
ts
// 1. 定义数据类型接口,模拟获取用户数据的函数
interface User {
id: number;
name: string;
}
interface Product {
id: number;
title: string;
price: number;
}
// 2. 泛型异步函数,泛型让这个函数可以获取任意类型的数据
// <T>:泛型类型参数
// url: string:普通参数
// Promise<T>:返回一个 Promise,resolve 的值类型为 T
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
// 解析 JSON,此时 data 类型是 any
const data = await response.json();
// 断言为 T 类型
return data as T;
}
// 3. 使用,使用时指定返回类型,
// T 为User
const user = await fetchData<User>('/api/user/1');
console.log(user.name); // TypeScript 知道 user 有 name 属性
// // T 为Product
const product = await fetchData<Product>('/api/product/1');
console.log(product.price); // TypeScript 知道 product 有 price 属性
- 合并两个对象
这个例子展示了泛型与交叉类型(Intersection Types)的结合使用,是对象合并的经典模式。
ts
// <T, U>:声明两个类型参数
// obj1: T:第一个对象类型为 T
// obj2: U:第二个对象类型为 U
// T & U:返回 T 和 U 的交叉类型(同时拥有两个类型的所有属性)
function mergeObjects<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const person = { name: "小明", age: 18 };
const address = { city: "北京", street: "长安街" };
const merged = mergeObjects(person, address);
// merged 的类型是 { name: string; age: number; city: string; street: string }
console.log(merged.name); // 小明
console.log(merged.city); // 北京
接口与类型别名
interface (接口)
接口(Interface)是 TypeScript 的核心特性,专门用于定义对象的结构形状。它像一个契约,规定了对象必须包含哪些属性以及这些属性的类型。接口在编译后会被完全删除,不会出现在最终的 JavaScript 代码中。
基础语法
ts
// 基本语法结构
interface 接口名 {
属性名: 类型;
属性名: 类型;
// ...
}
// 示例:定义用户接口
interface User {
id: number;
name: string;
age: number;
}
// 使用接口
const userInfo: User = {
id: 1,
name: "Alice",
age: 25
};
可选属性(Optional Properties)
在实际开发中,很多对象的属性并不是必需的。比如用户信息,可能只有 name 是必需的,age、email 等是可选的。
ts
// 语法结构
interface 接口名 {
属性名: 类型; // 必需属性
属性名?: 类型; // 在属性名后面加个问号 ? 表示可选属性,可以不提供该属性
// ...
}
interface Config {
url: string; // 必需属性
method?: string; // 可选属性,可以不提供
timeout?: number; // 可选属性
}
// 以下都是合法的
const config1: Config = { url: "https://api.com" };
const config2: Config = { url: "https://api.com", method: "POST" };
const config3: Config = { url: "https://api.com", timeout: 5000 };
可选属性的含义:
- 属性可以存在,也可以不存在
- 如果存在,类型必须匹配
- 访问可选属性时,TypeScript 会提示可能为 undefined
只读属性(Readonly Properties)
有些属性在对象创建后就不应该被修改,比如数据库记录的 ID、创建时间戳等。
ts
// 语法结构
interface 接口名 {
readonly 属性名: 类型; // 在属性名前添加 readonly ,表示只读属性,创建后不可修改
属性名: 类型;
// ...
}
interface Product {
readonly id: number; // 只读,创建后不可修改
name: string;
price: number;
readonly createdAt: Date; // 只读时间戳
}
const item: Product = {
id: 1001,
name: "Laptop",
price: 5999,
createdAt: new Date()
};
item.price = 5499; // 可以修改普通属性
item.id = 1002; // 错误:无法为只读属性赋值
item.createdAt = new Date(); // 错误:无法为只读属性赋值
进阶用法:使用 ReadonlyArray 和 Readonly
ts
interface Data {
values: readonly number[]; // 只读数组 - 不能修改数组内容
// 对象递归只读,等价于:{ readonly apiKey: string;}
config: Readonly<{ apiKey: string }>;
}
索引签名(Index Signatures)
当你不确定对象会有哪些属性名,但知道属性值的类型时,索引签名就非常有用。
ts
// 场景1:字典/映射结构
interface StringDictionary {
[key: string]: string; // 所有属性名都是字符串,属性值都是字符串
}
const dict: StringDictionary = {
name: "Alice",
city: "Beijing",
country: "China"
// 可以添加任意多个字符串属性
};
// 场景2:混合使用确定属性和索引签名
interface Student {
name: string; // 确定的属性
age: number;
[subject: string]: any; // 其他任意属性,值类型为 any
}
const student: Student = {
name: "Bob",
age: 20,
math: 95, // 任意属性
english: 88, // 任意属性
physics: 92 // 任意属性
};
// 场景3:数字索引签名(用于类数组对象)
// 数字索引的返回值类型必须是字符串索引返回值类型的子类型
interface NumberArray {
[index: number]: string; // 索引是数字,值是字符串
}
const myArray: NumberArray = ["a", "b", "c"];
console.log(myArray[0]); // "a"
// 数字索引的返回值必须是字符串索引返回值类型的子类型。
// 类型兼容:值类型一样
interface Example {
[index: string]: string; // 字符串索引
[index: number]: string; // 数字索引
}
// 类型兼容:any 可以兼容任何类型
interface AnyIndex {
[key: string]: any;
[index: number]: number; // number 兼容 any
}
// 类型兼容:联合类型包含所有可能
interface UnionIndex {
[key: string]: string | number;
[index: number]: number; // number 是联合类型的子集 ✅
}
type(类型别名)
核心概念
类型别名是为任何类型创建的一个新名字。相比接口只能描述对象/函数,类型别名可以描述所有类型,包括原始类型、联合类型、元组等。
基本语法与使用场景
ts
// 基本语法
type 类型别名 = 类型;
let 变量名: 类型别名 = 值
// 示例
type Name = string;
// 使用
let username: Name = "Alice";
- 联合类型(Union Types):一个值可能是多种类型之一
ts
// 基础联合
type StringOrNumber = string | number;
type Result = "success" | "error" | "loading";
// 复杂联合
interface Dog { type: "dog"; bark(): void; }
interface Cat { type: "cat"; meow(): void; }
type Pet = Dog | Cat;
function makeSound(pet: Pet) {
if (pet.type === "dog") {
pet.bark(); // TypeScript 知道这里是 Dog
} else {
pet.meow(); // TypeScript 知道这里是 Cat
}
}
- 元组类型(Tuple Types):固定长度、固定顺序、每个位置类型可不同
ts
// 基础元组
type RGB = [number, number, number]; // 三个数字,表示颜色
const red: RGB = [255, 0, 0];
// 带标签的元组(TypeScript 4.0+)
type Person = [name: string, age: number, isActive: boolean];
const alice: Person = ["Alice", 25, true];
// 可选元素的元组
type OptionalTuple = [string, number?];
const t1: OptionalTuple = ["hello"];
const t2: OptionalTuple = ["hello", 42];
// 剩余元素的元组
type StringNumberBooleans = [string, number, ...boolean[]];
const arr1: StringNumberBooleans = ["hello", 42];
const arr2: StringNumberBooleans = ["hello", 42, true, false, true];
- 交叉类型(Intersection Types): 合并多个类型
ts
type Person = {
name: string;
age: number;
};
type Employee = {
employeeId: number;
department: string;
};
type EmployeePerson = Person & Employee;
// 使用
const worker: EmployeePerson = {
name: "Alice",
age: 30,
employeeId: 12345,
department: "Engineering"
};
- 对象类型
ts
// 定义对象结构
type User = {
id: number;
name: string;
email: string;
age?: number; // 可选属性
readonly createdAt: Date; // 只读属性
};
// 使用
const user: User = {
id: 1,
name: "Bob",
email: "bob@example.com",
createdAt: new Date()
// age 是可选的,可以不提供
};
- 函数类型
ts
// 定义函数类型
type GreetFunction = (name: string) => string;
type MathOperation = (a: number, b: number) => number;
// 使用
const greet: GreetFunction = (name) => {
return `Hello, ${name}!`;
};
const add: MathOperation = (a, b) => a + b;
const multiply: MathOperation = (a, b) => a * b;
console.log(greet("Alice")); // "Hello, Alice!"
console.log(add(5, 3)); // 8
- 泛型类型别名
ts
// 泛型类型
type Box<T> = {
value: T;
getValue: () => T;
};
type Result<T, E = Error> = {
success: boolean;
data?: T;
error?: E;
};
// 使用
const numberBox: Box<number> = {
value: 42,
getValue() {
return this.value;
}
};
const stringBox: Box<string> = {
value: "hello",
getValue() {
return this.value;
}
};
const result: Result<User> = {
success: true,
data: { id: 1, name: "Alice" }
};
- 其他
ts
// 字面量
type Color = "red" | "green" | "blue"; // 字符串字面量
type Direction = "north" | "south" | "east" | "west"
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6; // 数字字面量
type StatusCode = 200 | 404 | 500;
// 索引类型
type StringDictionary = {
[key: string]: string;
};
type NumberDictionary = {
[key: string]: number;
};
// 混合索引(需兼容)
type MixedDictionary = {
[key: string]: string | number;
[index: number]: string; // 数字索引返回 string
};
// 使用
const dict: StringDictionary = {
name: "Alice",
city: "Beijing",
country: "China"
};
实际应用:React useState 的返回值类型
ts
// 这是一个泛型元组类型
// T: 第一个元素:状态值,类型为 T
//(newValue: T) => void: 第二个元素:更新函数,接收 T 类型参数
// 例子:type NumberUseState = UseStateReturn<number>;
// 等价于:[number, (newValue: number) => void]
type UseStateReturn<T> = [T, (newValue: T) => void];
// const useState - 声明常量
// : <T>(initial: T) => UseStateReturn<T> - 类型注解
// <T>:泛型参数,调用时自动推断
// initial: T:参数类型为 T
// UseStateReturn<T>:返回元组 [T, (newValue: T) => void]
// = (initial) => {...} - 箭头函数实现
const useState: <T>(initial: T) => UseStateReturn<T> = (initial) => {
let state = initial;
const setState = (newValue: typeof state) => { state = newValue; };
return [state, setState];
};
接口继承 vs 类型交织
继承(extends)
TypeScript 的继承是指一个类或接口可以从另一个类或接口获取属性和方法,实现代码复用和类型扩展。
TypeScript 支持两种继承:类的继承和接口的继承。
此次我们先讲接口的继承,后面讲到类再讲类继承
接口继承(extends)
核心思想:创建一个更具体的子接口,继承(复制)父接口的所有成员。
核心语法
ts
// 基础语法
// 使用关键字extends,继承了的子接口拥有父接口所有属性和方法
interface ChildInterface extends ParentInterface {
// 子接口自己的成员
newProperty: string;
}
基础继承
ts
// 基础继承
interface Animal {
name: string;
eat(): void;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
const myDog: Dog = {
name: "旺财", // 来自 Animal
breed: "金毛", // 来自 Dog
eat() { // 来自 Animal
console.log("吃狗粮");
},
bark() { // 来自 Dog
console.log("汪汪");
}
};
多接口继承:
ts
interface Flyable {
fly(): void;
getSpeed(): number;
}
interface Swimmable {
swim(): void;
getSpeed(): number;
}
// 继承多个接口,自动合并成员
interface Duck extends Flyable, Swimmable {
name: string;
}
const duck: Duck = {
name: "唐老鸭",
fly() { console.log("飞行"); },
swim() { console.log("游泳"); },
getSpeed() { return 10; } // 同时满足两个接口的要求
};
类型交织 (&)
核心思想:将多个类型合并成一个新类型
ts
// 基础交织
type Animal = {
name: string;
eat(): void;
};
type Dog = Animal & {
breed: string;
bark(): void;
};
// 和继承达到类似效果
const myDog: Dog = {
name: "旺财",
breed: "金毛",
eat() {},
bark() {}
};
复杂类型的交织:
ts
// 联合类型的交织
type A = { a: number } | { b: string };
type B = { c: boolean };
type C = A & B; // 结果是 { a: number; c: boolean } | { b: string; c: boolean }
// 函数类型的交织(实际是函数重载)
type LogFunction = {
(message: string): void;
(message: string, level: number): void;
};
type TimestampLog = {
(message: string): void;
timestamp: number;
};
type AdvancedLog = LogFunction & TimestampLog;
// 现在可以既作为函数调用,又带有 timestamp 属性
注意事项
- 同名属性处理
ts
// 接口继承:子接口的同名属性必须兼容父接口
interface A { value: string; }
interface B extends A { value: number; } // 错误!number 不能赋值给 string
// 类型交织:同名属性会求交集
type A = { value: string; };
type B = A & { value: number; }; // value 的类型是 string & number = never
const obj: B = { value: "hello" }; // 仍会报错,因为 never 类型没有值
- 声明合并能力
ts
// 接口支持声明合并
interface User { name: string; }
interface User { age: number; }
// 最终 User 类型同时有 name 和 age
// 类型不支持重复声明
type User { name: string; }
type User { age: number; } // 错误:Duplicate identifier 'User'
- 递归引用
ts
// 两者都支持递归引用
interface TreeNode {
value: string;
children?: TreeNode[]; // 接口可以递归
}
type Tree = {
value: string;
children?: Tree[]; // 类型别名也可以递归
};
// 但类型别名在更复杂的情况下可能有问题
type List = number | List[]; // 某些旧版本可能有警告
实际项目中的选择策略
使用 interface 的场景
ts
// 1. 定义公开 API 的对象结构
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 2. 定义类的契约
interface Repository<T> {
find(id: string): Promise<T>;
save(entity: T): Promise<void>;
delete(id: string): Promise<void>;
}
// 3. 需要利用声明合并的场景
// 例如扩展第三方库的类型
declare module 'some-library' {
interface LibraryOptions {
newFeature?: boolean;
}
}
// 4. 面向对象设计
interface Shape {
area(): number;
perimeter(): number;
}
使用 type 的场景
ts
// 1. 联合类型
type Status = "idle" | "loading" | "success" | "error";
type ID = string | number;
// 2. 元组类型
type Coordinate = [number, number, number]; // 3D 坐标
type HttpHeaders = [string, string][]; // 键值对数组
// 3. 函数类型(更简洁)
type EventHandler = (event: Event) => void;
type Middleware = (req: Request, res: Response, next: () => void) => void;
// 4. 映射类型
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 5. 条件类型
type IsString<T> = T extends string ? true : false;
// 6. 工具类型的组合
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
type UserWithoutId = Omit<User, 'id'>;
| 场景 | 推荐 | 理由 |
|---|---|---|
| 定义对象形状 | interface | 语义清晰,支持声明合并 |
| 定义类的结构 | interface | 面向对象设计的主要方式 |
| 库的公共 API | interface | 用户可以扩展和声明合并 |
| 联合类型 | type | interface 不支持 |
| 元组类型 | type | interface 不支持 |
| 原始类型别名 | type | interface 不支持 |
| 复杂类型操作 | type | 支持映射、条件等高级特性 |
| React Props/State | 都可以 | 大多数项目使用 interface,但 type 也可以 |
| 函数类型 | 都可以 | 个人/团队偏好 |
通用原则:
默认使用 interface,直到需要 type 特有的特性
保持团队一致性,选择合适的规范
对于公共库,优先使用 interface 以便用户扩展
类
属性与构造器 (public/private/protected)
TypeScript 提供了三种访问修饰符来控制类成员(属性和方法)的可见性。这是面向对象封装特性的核心体现。
- public - 公有成员
含义:public 修饰的成员可以在任何地方被访问,包括类内部、子类内部以及类外部的代码。在 TypeScript 中,如果不指定任何修饰符,成员默认就是 public 的。
ts
class BankAccount {
public accountNumber: string; // 账户号,需要对外公开
public balance: number; // 余额,需要对外查询
constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
// 公有方法,客户可以调用
public deposit(amount: number): void {
this.balance += amount;
console.log(`存入 ${amount} 元,当前余额 ${this.balance} 元`);
}
public withdraw(amount: number): boolean {
if (amount <= this.balance) {
this.balance -= amount;
console.log(`取出 ${amount} 元,剩余 ${this.balance} 元`);
return true;
}
console.log("余额不足");
return false;
}
}
const myAccount = new BankAccount("622202****1234", 10000);
// 外部可以直接访问 public 属性
console.log(myAccount.accountNumber); // 可以访问
console.log(myAccount.balance); // 可以访问
myAccount.deposit(500); // 可以调用
myAccount.withdraw(200); // 可以调用
- private - 私有成员
含义:private 修饰的成员只能在声明它的类内部被访问。即使是该类的子类也无法访问父类的私有成员。这是最严格的访问控制级别。
ts
class BankAccount {
private password: string; // 密码,绝不可对外暴露
private transactionHistory: string[] = []; // 交易记录,只读不写
constructor(accountNumber: string, password: string) {
this.password = password;
}
// 对外提供验证方法,但不暴露密码本身
public validatePassword(inputPwd: string): boolean {
// 可以访问 private 成员
return this.password === inputPwd;
}
private addTransaction(description: string): void {
// 私有方法,仅内部使用
this.transactionHistory.push(`${new Date().toLocaleString()}: ${description}`);
}
public deposit(amount: number): void {
this.balance += amount;
this.addTransaction(`存入 ${amount} 元`); // 内部可以调用私有方法
}
}
class SavingsAccount extends BankAccount {
constructor(accountNumber: string, password: string) {
super(accountNumber, password);
}
public someMethod(): void {
// 错误!子类不能访问父类的私有成员
// console.log(this.password);
// 错误!子类也不能调用父类的私有方法
// this.addTransaction("测试");
}
}
const account = new BankAccount("123456", "mypassword");
// 错误!外部不能访问私有属性
// console.log(account.password);
// 错误!外部不能调用私有方法
// account.addTransaction("外部操作");
// 只能通过公有方法间接操作
account.validatePassword("mypassword"); // true
private 的两种形式:
TypeScript 的 private 是编译时的检查,编译成 JavaScript 后就没有了。如果你想要真正运行时的私有性,可以使用 ES2022 的 # 私有字段:
ts
class ModernAccount {
#password: string; // 真正的私有字段,运行时也无法访问
constructor(password: string) {
this.#password = password;
}
checkPassword(pwd: string): boolean {
return this.#password === pwd;
}
}
const acc = new ModernAccount("123");
// console.log(acc.#password); // 语法错误
- protected - 受保护成员
含义:protected 修饰的成员可以在声明它的类内部以及该类的子类内部被访问,但不能在类外部被访问。它介于 public 和 private 之间。
ts
class Animal {
public name: string;
protected age: number; // 子类可以访问,外部不可见
private dna: string; // 连子类都不可见
constructor(name: string, age: number) {
this.name = name;
this.age = age;
this.dna = "ACGT...";
}
public eat(): void {
console.log(`${this.name} 正在进食`);
this.digest(); // 调用私有方法
}
private digest(): void {
console.log("消化中...");
}
protected sleep(): void { // 受保护方法,子类可以重载或使用
console.log(`${this.name} 正在睡觉`);
}
}
// 子类Dog继承(extends)父类Animal
class Dog extends Animal {
private breed: string;
constructor(name: string, age: number, breed: string) {
super(name, age);
this.breed = breed;
}
public displayInfo(): void {
console.log(`名字: ${this.name}`); // public,可以访问
console.log(`年龄: ${this.age}`); // protected,子类可以访问
// console.log(this.dna); // private,子类不能访问
// ✅ 可以调用受保护的方法
this.sleep(); // 基类中定义的 sleep 方法
}
// 重写受保护方法
protected sleep(): void {
console.log(`${this.name} 像狗一样蜷缩着睡觉`);
super.sleep(); // 也可以调用父类版本
}
public takeNap(): void {
this.sleep(); // 调用自己的 sleep
}
}
const dog = new Dog("旺财", 3, "金毛");
dog.eat(); // 公有方法
dog.displayInfo(); // 名字: 旺财, 年龄: 3
// dog.sleep(); // 错误!protected 方法外部不能调用
// console.log(dog.age); // 错误!protected 属性外部不能访问
- 三种修饰符对比总结
| 修饰符 | 类内部 | 子类内部 | 类外部 | 典型用途 |
|---|---|---|---|---|
| public | ✅ | ✅ | ✅ | API接口、对外属性 |
| protected | ✅ | ✅ | ❌ | 继承使用的属性、模板方法 |
| private | ✅ | ❌ | ❌ | 内部状态、实现细节 |
特殊修饰符详解 readonly, static, abstract
- readonly - 只读属性
**含义:**readonly 修饰的属性只能在声明时或构造函数中被初始化,之后就不能再被修改。它提供了不可变性的保障。
重要特点:
- 可以在声明时直接赋值
- 也可以在构造函数中赋值
- 一旦初始化完成,任何地方都不能再修改
- 常与 public、private、protected 组合使用
ts
class Configuration {
// 声明时直接赋值
public readonly APP_NAME: string = "MyApp";
public readonly VERSION: string = "1.0.0";
// 构造函数中赋值
public readonly CREATED_AT: Date;
private readonly SECRET_KEY: string;
constructor(secretKey: string) {
this.CREATED_AT = new Date();
this.SECRET_KEY = secretKey;
}
public getSecretKey(): string {
// 错误!readonly 属性不能修改
// this.SECRET_KEY = "new value";
// this.APP_NAME = "NewName";
// 可以读取
return this.SECRET_KEY;
}
public updateConfig(): void {
// 所有 readonly 属性都不可修改
// this.VERSION = "2.0.0";
// this.CREATED_AT = new Date();
}
}
class SubConfig extends Configuration {
constructor(secretKey: string) {
super(secretKey);
// 错误!子类也无法修改父类的 readonly 属性
// this.APP_NAME = "SubApp";
}
}
const config = new Configuration("abc123");
console.log(config.APP_NAME); // 可以读取
console.log(config.VERSION); // 可以读取
// config.APP_NAME = "NewApp"; // 错误!无法修改
readonly vs const:
- const 用于常量,在编译时就确定值,且必须是字面量
- readonly 用于实例属性,可以在运行时确定值,每个实例可以不同
ts
class Example {
const MAX_SIZE: number = 100; // 报错;类中不能用 const
static readonly MAX_SIZE: number = 100; // 正确;静态只读属性
readonly instanceId: string;
constructor() {
// 可以在运行时生成唯一ID
this.instanceId = Math.random().toString(36);
}
}
- static - 静态成员
含义: static 修饰的成员**属于类本身而不是类的实例。**静态成员通过类名直接访问,不需要创建实例。所有实例共享同一个静态成员。
ts
class MathUtils {
// 静态常量
static readonly PI: number = 3.141592653589793;
static readonly E: number = 2.718281828459045;
// 静态方法 - 工具函数
static add(x: number, y: number): number {
return x + y;
}
static multiply(x: number, y: number): number {
return x * y;
}
static circleArea(radius: number): number {
return this.PI * radius * radius; // 静态方法中可以用 this 访问其他静态成员
}
}
// 通过类名直接调用,不需要创建实例
console.log(MathUtils.PI); // 3.14159...
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.circleArea(5)); // 78.5398...
// 静态成员在实例中不可用
const utils = new MathUtils(); // 虽然可以实例化,但没有意义
// console.log(utils.PI); // ❌ 错误!实例不能访问静态属性
// utils.add(1, 2); // ❌ 错误!实例不能调用静态方法
class User {
// 静态属性 - 跟踪所有用户
private static totalUsers: number = 0;
private static allUsers: User[] = [];
// 实例属性
public readonly id: number;
private name: string;
constructor(name: string) {
this.id = ++User.totalUsers; // 自动生成ID
this.name = name;
User.allUsers.push(this);
}
// 静态方法 - 获取所有用户
static getAllUsers(): User[] {
return [...User.allUsers]; // 返回副本,避免外部修改
}
// 静态方法 - 统计用户数
static getUserCount(): number {
return User.totalUsers;
}
// 静态方法 - 根据ID查找用户
static findById(id: number): User | undefined {
return User.allUsers.find(user => user.id === id);
}
// 静态工厂方法
static createAdmin(name: string): User {
const admin = new User(name);
console.log(`创建管理员: ${name}`);
return admin;
}
// 实例方法
getName(): string {
return this.name;
}
}
const user1 = new User("张三");
const user2 = new User("李四");
const admin = User.createAdmin("王五");
console.log(User.getUserCount()); // 3
console.log(User.findById(2)?.getName()); // 李四
// 静态方法中的 this 指向
class StaticThis {
static value: number = 10;
static method1(): void {
console.log(this.value); // this 指向 StaticThis 类
}
static method2(): void {
const fn = () => {
console.log(this.value); // 箭头函数中的 this 指向外部作用域(仍然是类)
};
fn();
}
static method3(): void {
function normalFunc() {
// console.log(this.value); // 错误;普通函数中的 this 是 undefined(严格模式)
}
normalFunc();
}
}
StaticThis.method1(); // 10
静态成员的重要特性:
- 静态成员可以被继承(子类可以访问父类的静态成员)
- 静态方法不能访问实例成员(因为没有实例)
- 实例方法可以访问静态成员(通过类名或 this.constructor)
ts
class Parent {
static familyName: string = "张";
static getFamilyName(): string {
return Parent.familyName;
}
}
class Child extends Parent {
static getInfo(): void {
console.log(this.familyName); // 子类可以访问父类的静态属性
console.log(super.getFamilyName()); // 可以通过 super 调用父类静态方法
}
}
console.log(Child.familyName); // "张" - 静态属性被继承
Child.getInfo(); // "张" "张"
- abstract - 抽象类
含义: abstract 关键字用于定义抽象类和抽象方法。抽象类是不能被实例化的类,它作为其他类的基类,定义通用的结构和行为。抽象方法只有声明没有实现,必须在派生类中实现。
核心特性:
- 抽象类不能直接创建实例
- 抽象方法必须在子类中实现
- 抽象类可以包含具体方法和具体属性
- 抽象类可以有构造函数(虽然不能直接实例化,但子类可以通过 super() 调用)
ts
// 定义抽象类
abstract class Shape {
// 抽象属性 - 子类必须实现
abstract readonly name: string;
// 实例属性
protected color: string;
constructor(color: string) {
this.color = color;
}
// 抽象方法 - 只有签名,没有实现
abstract getArea(): number;
abstract getPerimeter(): number;
// 具体方法 - 有完整实现,子类可以选择重写
getColor(): string {
return this.color;
}
// 具体方法 - 子类不能重写(可用 final 概念模拟)
describe(): string {
return `这是一个${this.color}色的${this.name},面积是${this.getArea()},周长是${this.getPerimeter()}`;
}
// 静态方法(抽象类可以有静态方法)
static compareArea(shape1: Shape, shape2: Shape): number {
return shape1.getArea() - shape2.getArea();
}
}
// 具体子类必须实现所有抽象成员
class Circle extends Shape {
readonly name: string = "圆形";
private radius: number;
constructor(color: string, radius: number) {
super(color);
this.radius = radius;
}
// 实现抽象方法
getArea(): number {
return Math.PI * this.radius * this.radius;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
// 可以添加自己的方法
getRadius(): number {
return this.radius;
}
}
class Rectangle extends Shape {
readonly name: string = "长方形";
private width: number;
private height: number;
constructor(color: string, width: number, height: number) {
super(color);
this.width = width;
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
// 可选:重写父类的具体方法
describe(): string {
return `${super.describe()} 尺寸: ${this.width}x${this.height}`;
}
}
// const shape = new Shape("red"); // ❌ 错误!抽象类不能实例化
const circle = new Circle("红色", 5);
const rectangle = new Rectangle("蓝色", 4, 6);
console.log(circle.describe());
// 输出:这是一个红色的圆形,面积是78.5398...,周长是31.4159...
console.log(rectangle.describe());
// 输出:这是一个蓝色的长方形,面积是24,周长是20 尺寸: 4x6
// 多态:使用抽象类型的变量指向具体子类的实例
let shapes: Shape[] = [circle, rectangle];
shapes.forEach(shape => {
console.log(`${shape.name}的面积: ${shape.getArea()}`);
});
// 抽象类作为参数类型
function printShapeInfo(shape: Shape): void {
console.log(shape.describe());
}
实现接口 (implements)
- 基本概念
含义: implements 关键字用于让一个类遵循某个接口的契约。接口定义了类应该具有的成员(属性、方法),类必须实现接口中声明的所有成员。
核心要点:
- 一个类可以实现多个接口
- 接口只定义结构,不提供实现
- 类可以实现多个接口,用逗号分隔
- 类在实现接口的同时还可以有自己的额外成员
- 接口可以继承其他接口
类实现单个接口
ts
// 定义接口
interface Drawable {
draw(): void;
getColor(): string;
}
// 类实现单个接口
class Circle implements Drawable {
private color: string;
private radius: number;
constructor(color: string, radius: number) {
this.color = color;
this.radius = radius;
}
// 必须实现接口的所有方法
draw(): void {
console.log(`画一个${this.color}色的圆形,半径${this.radius}`);
}
getColor(): string {
return this.color;
}
// 可以有自己的额外方法
getArea(): number {
return Math.PI * this.radius * this.radius;
}
}
类实现多个接口
ts
// 定义接口
interface Resizable {
resize(width: number, height: number): void;
getSize(): { width: number; height: number };
}
interface Clickable {
onClick(handler: () => void): void;
}
// 类实现多个接口
class Rectangle implements Drawable, Resizable {
private color: string;
private width: number;
private height: number;
constructor(color: string, width: number, height: number) {
this.color = color;
this.width = width;
this.height = height;
}
// 实现 Drawable
draw(): void {
console.log(`画一个${this.color}色的长方形`);
}
getColor(): string {
return this.color;
}
// 实现 Resizable
resize(width: number, height: number): void {
this.width = width;
this.height = height;
console.log(`调整尺寸为 ${width}x${height}`);
}
getSize(): { width: number; height: number } {
return { width: this.width, height: this.height };
}
// 额外方法
getArea(): number {
return this.width * this.height;
}
}
- 接口继承与类实现
ts
// 接口可以继承其他接口
interface Shape extends Drawable, Resizable {
getArea(): number;
getPerimeter(): number;
}
// 类实现继承后的接口
class Pentagon implements Shape {
private color: string;
private side: number;
constructor(color: string, side: number) {
this.color = color;
this.side = side;
}
// 实现 Drawable
draw(): void {
console.log(`画五边形`);
}
getColor(): string {
return this.color;
}
// 实现 Resizable
resize(width: number, height: number): void {
this.side = Math.min(width, height) / 2;
}
getSize(): { width: number; height: number } {
return { width: this.side * 2, height: this.side * 2 };
}
// 实现 Shape 添加的方法
getArea(): number {
return (5/4) * Math.tan(54 * Math.PI/180) * this.side * this.side;
}
getPerimeter(): number {
return 5 * this.side;
}
}
- 接口与类的区别
ts
// 接口定义可选属性、只读属性
interface UserInterface {
readonly id: number; // 只读
name: string;
age?: number; // 可选
email: string;
[key: string]: any; // 索引签名
}
// 类实现接口时可以添加更多特性
class UserClass implements UserInterface {
readonly id: number;
name: string;
email: string;
age?: number;
// 可以添加构造函数和额外方法
constructor(id: number, name: string, email: string) {
this.id = id;
this.name = name;
this.email = email;
}
// 额外的方法
greet(): string {
return `Hello, I'm ${this.name}`;
}
}
// 接口可以定义函数类型
interface Comparator<T> {
(a: T, b: T): number;
}
class Sorter {
sort<T>(items: T[], comparator: Comparator<T>): T[] {
return [...items].sort(comparator);
}
}
// 类可以实现描述构造函数的接口
interface Constructor {
new (name: string): Person;
}
class Person {
constructor(public name: string) {}
}
function createPerson(ctor: Constructor, name: string): Person {
return new ctor(name);
}
const p = createPerson(Person, "张三");
- implements vs extends
ts
// extends: 继承实现(获得父类的代码)
// implements: 实现接口(只获得类型约束)
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(): void {
console.log(`${this.name} 在移动`);
}
}
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
// 可以同时继承类和实现接口
class Duck extends Animal implements Flyable, Swimmable {
constructor(name: string) {
super(name);
}
// 实现接口方法
fly(): void {
console.log(`${this.name} 在飞翔`);
}
swim(): void {
console.log(`${this.name} 在游泳`);
}
// 可以重写父类方法
move(): void {
console.log(`${this.name} 走来走去`);
super.move();
}
}
参数属性
- 基本概念与用法
含义:参数属性是 TypeScript 提供的一种语法糖,允许在构造函数参数中直接声明和初始化类的成员变量。通过在构造函数参数前添加访问修饰符(public、private、protected)或 readonly,TypeScript 会自动创建同名属性并赋值。
ts
// 传统写法 - 冗长
class Employee1 {
public id: number;
private name: string;
protected department: string;
readonly hireDate: Date;
public salary: number;
constructor(id: number, name: string, department: string, hireDate: Date, salary: number) {
this.id = id;
this.name = name;
this.department = department;
this.hireDate = hireDate;
this.salary = salary;
}
}
// 参数属性写法 - 简洁
class Employee2 {
constructor(
public id: number,
private name: string,
protected department: string,
readonly hireDate: Date,
public salary: number = 0 // 可以有默认值
) {
// 不需要手动赋值,TypeScript 会自动处理
// 这里的代码在属性初始化之后执行
console.log(`创建员工: ${this.name}`); // 可以访问参数属性
}
getInfo(): string {
return `${this.name} (ID: ${this.id}) - ${this.department}`;
}
// 可以修改非 readonly 的属性
promote(newSalary: number): void {
this.salary = newSalary;
// this.hireDate = new Date(); // 错误;readonly 不能修改
}
}
// 使用
const emp = new Employee2(1, "张三", "技术部", new Date(), 8000);
console.log(emp.id); // 1 - public
console.log(emp.salary); // 8000 - public
// console.log(emp.name); // 错误;private
// console.log(emp.department); // 错误;protected
console.log(emp.hireDate); // readonly 可以被外部读取
- 参数属性的高级用法
- 混合使用参数属性和普通属性
ts
class Product {
private static nextId: number = 1;
// 参数属性自动创建
constructor(
public name: string,
private price: number,
protected category: string,
public readonly sku: string, // 只读参数属性
) {
// 可以添加额外的逻辑
if (price < 0) {
throw new Error("价格不能为负数");
}
}
// 普通属性(不是通过参数创建的)
public id: number = Product.nextId++;
public createdAt: Date = new Date();
// 普通属性但使用参数初始化
private discount: number;
constructor(name: string, price: number, category: string, sku: string, discount: number) {
this.name = name;
this.price = price;
this.category = category;
this.sku = sku;
this.discount = discount;
}
}
- 参数属性与默认值
ts
class Config {
constructor(
public host: string = "localhost",
public port: number = 3000,
private secure: boolean = false,
readonly version: string = "1.0.0"
) {}
}
const defaultConfig = new Config();
console.log(defaultConfig.host); // "localhost"
const customConfig = new Config("example.com", 8080, true, "2.0.0");
console.log(customConfig.host); // "example.com"
- 参数属性与可选参数
ts
class SearchParams {
constructor(
public query: string,
public page?: number, // 可选参数属性
public pageSize?: number,
public sortBy?: string,
public order?: "asc" | "desc"
) {}
getParams(): Record<string, string | number> {
const params: Record<string, string | number> = {
query: this.query
};
if (this.page !== undefined) params.page = this.page;
if (this.pageSize !== undefined) params.pageSize = this.pageSize;
if (this.sortBy !== undefined) params.sortBy = this.sortBy;
if (this.order !== undefined) params.order = this.order;
return params;
}
}
- 参数属性与解构(高级技巧)
ts
class DataRow {
constructor(
public id: number,
public data: { name: string; value: number },
private meta?: { created: Date; author: string }
) {}
getMeta() {
return this.meta || { created: new Date(), author: "system" };
}
}
- 参数属性与依赖注入(常见模式)
ts
class UserService {
constructor(
private userRepository: UserRepository,
private logger: Logger,
private config: Config
) {}
async getUser(id: number): Promise<User | null> {
this.logger.log(`获取用户: ${id}`);
return this.userRepository.findById(id);
}
}
// 实际使用
const userRepo = new UserRepository();
const logger = new Logger();
const config = new Config();
const userService = new UserService(userRepo, logger, config);
泛型
上面函数里有提到过,现在复习一下及系统地了解。
**泛型允许我们在定义接口或类时,不预先指定具体类型,而是留到使用时再确定。**这样可以提高代码的复用性和类型安全。
一句话:不预先固定类型,而是在使用时才确定类型。
语法
ts
// <T> 就像一个"类型的占位符",调用时才决定它具体是什么类型。
// value: T 说:参数的类型就是那个占位符。
// : T 说:返回的类型也和参数一样。
function identity<T>(value: T): T {
return value
}
// 调用时:
let a = identity(123) // 类型为 number
let b = identity("hello") // 类型为 string
// 效果:一个函数,适配所有类型,同时保留精确的类型信息。
函数泛型
ts
// 尖括号在函数名后,参数列表前
function identity<T>(arg: T): T {
return arg;
}
// 箭头函数(普通)
const identityArrow = <T>(arg: T): T => arg;
// 箭头函数在 .tsx 文件中的特殊语法(避免歧义)
const identityTSX = <T,>(arg: T): T => arg; // 加一个逗号
// 或使用 extends {} 辅助
const identityTSX2 = <T extends {}>(arg: T): T => arg;
接口泛型
ts
// 接口名后跟 <类型参数>
interface Box<T> {
value: T;
get(): T;
}
// 使用
let box: Box<string> = { value: "hi", get() { return this.value; } };
类型别名泛型
ts
// 类型别名后跟 <类型参数>
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
// 使用
let res: Result<number> = { ok: true, value: 42 };
类泛型
ts
// 类名后跟 <类型参数>
class Stack<T> {
private items: T[] = [];
push(item: T) { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
// 静态成员不能使用类的泛型参数(静态属于类本身,而非实例)
class Box<T> {
static defaultValue: T; // 错误:静态成员不能引用类型参数 T
}
- 泛型类的类型参数可以在方法、属性、构造函数中使用。
- 泛型类可以继承,子类可以选择保留或固定父类的泛型参数。
泛型约束 extends
默认的泛型可以接受任何类型,但某些操作需要类型具备特定的属性或方法(比如 .length、.toString())。约束可以限定泛型必须满足某个接口。
- 约束属性/方法
ts
interface HasLength {
length: number;
}
// 只允许具有 length 属性的类型
function logLength<T extends HasLength>(item: T): T {
console.log(item.length);
return item;
}
logLength("hello"); // 字符串有 length → 合法
logLength([1, 2, 3]); // 数组有 length → 合法
logLength({ length: 10 }); // 合法
// logLength(123); // 错误:number 没有 length 属性
- 约束构造函数(可实例化的类型)
ts
// 约束 T 必须具有无参构造函数
function createInstance<T>(ctor: { new (): T }): T {
return new ctor();
}
class Person {
name = "John";
}
const john = createInstance(Person); // john 类型为 Person
console.log(john.name); // "John"
- 约束类型参数之间的关系
一个泛型参数可以约束另一个:
ts
// 确保 K 是 T 的键(key)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const nameValue = getProperty(user, "name"); // 类型为 string
// getProperty(user, "email"); // 错误:email 不是 user 的键
- 使用 keyof 与约束结合
ts
// 将对象的某个属性映射为其他值
function mapValue<T, K extends keyof T>(
obj: T,
key: K,
transform: (value: T[K]) => T[K]
): T {
return { ...obj, [key]: transform(obj[key]) };
}
const obj = { a: 1, b: "hello" };
const newObj = mapValue(obj, "a", (x) => x + 10); // { a: 11, b: "hello" }
// 第二个参数必须是 "a" 或 "b",否则类型错误
多个类型参数
- 基本语法与场景
多个类型参数用逗号分隔,通常用于需要同时处理多种不同类型的情况。
- 交换元组中的类型
ts
function swap<T, U>(pair: [T, U]): [U, T] {
return [pair[1], pair[0]];
}
const swapped = swap([1, "hello"]); // 类型为 [string, number],实际值为 ["hello", 1]
- 键值映射函数
ts
// 将数组中的每个元素转换为键值对
function toPairs<T, K extends string | number | symbol, V>(
items: T[],
keySelector: (item: T) => K,
valueSelector: (item: T) => V
): Record<K, V> {
const result = {} as Record<K, V>;
for (const item of items) {
result[keySelector(item)] = valueSelector(item);
}
return result;
}
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const userMap = toPairs(users, u => u.id, u => u.name);
// 类型为 Record<number, string>,值为 { 1: "Alice", 2: "Bob" }
- 类型参数之间的依赖与约束
后面的参数可以依赖于前面的参数,例如 K extends keyof T。
ts
// 复制对象的指定属性到新对象
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = obj[key];
}
return result;
}
const person = { name: "Tom", age: 25, city: "NYC" };
const picked = pick(person, "name", "age"); // 类型为 Pick<{...}, "name"|"age">
// picked.city // 错误:city 不存在于返回类型中
- 高级模式:柯里化与类型安全
ts
// 柯里化函数,逐步固化类型
function curry<T, U, R>(fn: (a: T, b: U) => R): (a: T) => (b: U) => R {
return (a: T) => (b: U) => fn(a, b);
}
const add = (x: number, y: number) => x + y;
const curriedAdd = curry(add);
const add5 = curriedAdd(5);
const result = add5(10); // 15
常见陷阱:多个类型参数时,TypeScript 的类型推断可能不完全,必要时可显式指定。
默认泛型参数
当你希望提供一种"最常用"的类型,同时保留让用户覆盖的能力时,默认泛型就很实用。类似于函数参数的默认值。
语法:<T = DefaultType>
- 接口中的默认泛型
ts
// 默认类型为 any,兼容旧代码
interface ApiResponse<T = any> {
code: number;
data: T;
message: string;
}
// 不指定 T 时使用 any
let unknownResponse: ApiResponse;
unknownResponse.data = "任意值"; // 无类型检查
// 指定具体类型时获得类型安全
let userResponse: ApiResponse<{ id: number }>;
userResponse = {
code: 200,
data: { id: 1 },
message: "OK",
};
- 类中的默认泛型
ts
class EventEmitter<TEventMap = Record<string, any>> {
private listeners: Map<keyof TEventMap, Function[]> = new Map();
on<K extends keyof TEventMap>(event: K, handler: (payload: TEventMap[K]) => void): void {
if (!this.listeners.has(event)) this.listeners.set(event, []);
this.listeners.get(event)!.push(handler);
}
emit<K extends keyof TEventMap>(event: K, payload: TEventMap[K]): void {
const handlers = this.listeners.get(event);
if (handlers) handlers.forEach(handler => handler(payload));
}
}
// 不指定具体事件类型,使用 Record<string, any>
const genericEmitter = new EventEmitter();
// 指定事件类型,获得类型安全
interface MyEvents {
click: { x: number; y: number };
change: string;
}
const typedEmitter = new EventEmitter<MyEvents>();
typedEmitter.on("click", (payload) => {
console.log(payload.x); //payload 类型为 { x: number; y: number }
});
// typedEmitter.on("hover", ...) // 错误:hover 不在 MyEvents 中
- 函数中的默认泛型
注意:函数中的默认泛型通常在类型参数无法推断且未显式指定时生效。但 TypeScript 会根据参数自动推断,默认值往往被覆盖。
ts
// 默认泛型为 number
function createArray<T = number>(length: number, value: T): T[] {
return Array(length).fill(value);
}
// 参数 value 为 "x"(字面量类型 "x"),推断出 T 为 "x",而不是 number
const arr1 = createArray(3, "x"); // 类型为 "x"[]
// 强制使用默认类型(不指定类型参数,且让 value 类型为可推断为默认类型?无法直接强制)
// 但可以这样利用:如果不想依赖推断,显式指定空类型参数
const arr2 = createArray<>(3, 100); // 此时 T 推断为 100(字面量),仍是字面量
// 真正触发默认类型的场景:不传泛型参数,且参数类型为 any?实际很少用到
更实用的函数默认泛型场景:高阶函数,比如创建一个"配置对象生成器"。
ts
interface Config {
timeout: number;
retries: number;
}
function createConfig<T extends Partial<Config> = Config>(overrides: T): T & Config {
const defaults: Config = { timeout: 5000, retries: 3 };
return { ...defaults, ...overrides };
}
const config1 = createConfig({ timeout: 1000 }); // 类型为 { timeout: number } & Config
const config2 = createConfig({ extra: true }); // 类型推断为 Config & { extra: boolean }
- 默认类型与约束同时使用
顺序:<T extends 约束类型 = 默认类型>
ts
// T 必须具有 length 属性,默认类型为 string
function logAndReturn<T extends { length: number } = string>(item: T): T {
console.log(item.length);
return item;
}
logAndReturn("abc"); // 使用默认类型 string(推断也是 string)
logAndReturn([1, 2, 3]); // 显式推断为 number[],符合约束
// logAndReturn(123); // 错误:number 没有 length
注意:默认类型也必须满足约束条件(否则 TS 会报错)。
泛型参数的作用域与嵌套
ts
// 外部泛型参数可以在内部使用
function outer<T>(val: T) {
function inner<U>(val2: U): [T, U] {
return [val, val2];
}
return inner;
}
// 泛型约束中可以使用前一个参数
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
泛型参数用于高阶类型
ts
// 泛型参数可以传给其他泛型
type Wrapped<T> = { data: T };
type DoublyWrapped<T> = Wrapped<Wrapped<T>>;
// 泛型参数也可以用于条件类型
type IsArray<T> = T extends any[] ? true : false;
typeof 与泛型的混合语法
ts
// 从变量推导类型,并用作泛型参数
const myObj = { x: 1, y: "hi" };
function wrap<T>(obj: T): { original: T } {
return { original: obj };
}
const wrapped = wrap(myObj); // T 自动推断为 typeof myObj
// 显式使用 typeof
const wrappedExplicit = wrap<typeof myObj>(myObj);
在 TSX/React 组件中
ts
// 函数组件(.tsx 文件中)
const MyComponent = <T,>(props: { data: T }): JSX.Element => {
return <div>{/* 使用 props.data */}</div>;
};
// 或者使用 extends 消除歧义
const MyComponent2 = <T extends {}>(props: { data: T }) => { ... };
泛型参数的命名惯例(非强制但常见)
T- 通用单个类型(Type)K- 键类型(Key)V- 值类型(Value)E- 元素类型(Element)R- 返回类型(Return)S,U- 第二个、第三个类型参数
语法要点总结
| 场景 | 语法示例 |
|---|---|
| 函数 | function fn<T>(arg: T): T {} |
| 箭头函数 | const fn = <T>(arg: T): T => arg |
| TSX 箭头函数 | const fn = <T,>(arg: T): T => arg |
| 接口 | interface IFace<T> { val: T } |
| 类型别名 | type Alias<T> = T[] |
| 类 | class Class<T> { prop: T } |
| 约束 | <T extends SomeType> |
| 默认类型 | <T = DefaultType> |
| 多个参数 | <T, K, V extends SomeType = string> |
| 作用域内引用前一个参数 | <T, K extends keyof T> |
高级类型
类型守卫
类型守卫是一组运行时检查方式,用于在特定作用域内缩窄(narrow)变量的类型。
简单说 ,类型守卫 是用于在代码块中缩小类型范围的技术。
- 类型守卫 -- typeof
适用于原始类型(string, number, boolean, symbol, bigint, function, object 但注意 typeof null 是 "object")。
语法模式: typeof 变量名 === "原始类型"
注:同样支持 !== 否定形式。此守卫只能用在 if、while 等条件中,TypeScript 会根据比较结果缩窄变量类型。
ts
function printValue(value: string | number) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // 此处 value 被推断为 string
} else {
console.log(value.toFixed(2)); // 此处 value 被推断为 number
}
}
- instanceof 类型守卫
用于检查一个对象是否是某个类的实例。
语法模式: 变量名 instanceof 构造函数名
构造函数名必须是类(或能作为构造函数的函数)。用在条件表达式中时,若返回 true,则将变量类型缩窄为该构造函数的实例类型。
ts
class Dog { bark() {} }
class Cat { meow() {} }
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // animal 是 Dog
} else {
animal.meow(); // animal 是 Cat
}
}
- in 操作符守卫
检查对象上是否存在某个属性,常用于区分具有不同属性的对象类型。
语法模式: "属性名" in 变量名
属性名是字符串字面量或字符串类型。当检查结果为 true 时,TypeScript 会认为该变量具有该属性,从而将其缩窄到包含该属性的类型(通常是联合类型中的某一成员)。
ts
interface Fish { swim: () => void }
interface Bird { fly: () => void }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // 此处 animal 为 Fish
} else {
animal.fly(); // 此处 animal 为 Bird
}
}
- 自定义类型守卫(is 语法)
当需要更复杂的判断逻辑时,可以写一个返回类型谓词 parameterName is Type 的函数。
语法模式: function 守卫名 ( 参数名 : any 或联合类型 ) : 参数名 is 具体类型 { ... }
- 函数返回类型必须写成
参数名 is 类型(参数名是函数参数列表中的某一个)。 - 函数体内的逻辑需要手工实现类型判断,并返回 boolean。
- 当函数返回
true时,TypeScript 会将在守卫调用所在作用域内的对应变量缩窄为is后面的类型。
ts
interface Cat {
meow(): void;
fur: string;
}
function isCat(animal: any): animal is Cat {
return (animal as Cat).meow !== undefined;
}
function handle(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow(); // 安全调用,animal 被缩窄为 Cat
console.log(animal.fur);
}
}
- asserts 断言守卫(TypeScript 3.7+)
使用 asserts 关键字,当条件不满足时抛出错误,从而在后续代码中确保类型。
语法模式: function 守卫名 ( 参数名 : any ) : asserts 参数名 is 具体类型 { ... }
- 与自定义类型守卫类似,但函数返回类型写成
asserts 参数名 is 类型 - 函数体内若条件不满足,必须抛出一个错误(如 throw new Error());若条件满足,则正常返回 void。
- 调用断言守卫后,从该调用点开始到当前作用域结束,TypeScript 会认为变量已缩窄为 is 后面的类型,无需再写 if 判断。
ts
function assertIsString(value: any): asserts value is string {
if (typeof value !== "string") {
throw new Error("Not a string!");
}
}
function upper(input: unknown) {
assertIsString(input);
// 此处 input 确定是 string 类型
console.log(input.toUpperCase());
}
- 真值缩窄(Truthiness narrowing)
虽然不是显式的守卫语法,但通过 if (variable) 可以排除 null、undefined、0、""、NaN 等假值,从而缩窄类型。
隐式规则:
将变量直接放在 if (变量)、while (变量)、变量 && ... 等真值测试位置时,TypeScript 会自动排除 null、undefined、0、NaN、""、false、0n 等假值。缩窄后的类型取决于变量原本的类型(例如 string | null 会缩窄为 string)。
ts
function printLength(str: string | null) {
if (str) {
// 此时 str 肯定不是 null,TypeScript 推断为 string
console.log(str.length);
}
}
小结:
| 方式 | 适用场景 | 示例 |
|---|---|---|
| typeof | 原始类型区分 | typeof x === "string" |
| instanceof | 类实例区分 | x instanceof Dog |
| in | 属性存在性检查 | "prop" in obj |
| 自定义 is | 复杂类型逻辑 | function isDog(x): x is Dog |
| asserts | 断言并抛出错误 | asserts x is string |
| 真值缩窄 | 排除假值 | if (x) |
合理使用类型守卫可以让 TypeScript 代码更加安全和优雅,避免不必要的类型断言(as)。
映射类型
映射类型,允许你基于已有类型的属性,通过遍历键集合来创建新类型。可以把它理解为类型层面的 map 操作。
映射类型 = 告诉 TS:"请对这个类型里的每一个属性,都自动执行某个规则。"
比如:
- 每个属性都变成可选的 → Partial
- 每个属性都变成只读的 → Readonly
- 每个属性都变成 string 类型 → { [K in keyof T]: string }
基础语法 :映射类型使用 keyof 和索引签名语法,形式为 { [P in K]: T }
P in K]遍历(映射):P 是一个变量,它会依次取联合类型 K 中的每个成员;in 类似 JS 的 for...in;K 必须是一个联合类型(比如 'a' | 'b' 或 keyof Something): T每个属性 P 的值类型都是 T(T 可以是任意类型)
ts
type Keys = 'name' | 'age';
type Person = {
[K in Keys]: string | number;
};
// 等价于 { name: string | number; age: string | number; }
常用内置映射类型
TypeScript 提供了多个实用的内置映射类型,避免手写的繁琐
ts
interface User {
id: number;
name: string;
email: string;
}
// Partial<T>:遍历现有类型 T 的每个属性,把它们变成可选。
// Partial<{ x: number }> → { x?: number }
type PartialUser = Partial<User>;
// Readonly<T>:遍历现有类型 T 的每个属性,把它们变成只读。
// Readonly<{ x: number }> → { readonly x: number }
type ReadonlyUser = Readonly<User>;
// Required<T>:所有字段变为必选 ;Required<{ x?: number }> → { x: number }
type RequiredUser = Required<User>
// Pick<T, K> 遍历现有类型 T 的每个属性,选取部分属性 K
// 只取 id 和 name
type UserBasic = Pick<User, 'id' | 'name'>;
// 等价于 { name: string; id: number; }
// Omit<T, K>: 遍历现有类型 T 的每个属性,排除部分属性 K
// 剔除了 email
type WithoutEmail = Omit<User, "email">;
// 等价于 { name: string; id: number; }
// Record<K, T>:创建键为 K、值为 T 的类型。
// 不依赖现有类型,而是根据你提供的键清单 K,创建一个全新的对象类型,所有值类型统一为 T。
// Record<K, T> 表示"创建一个对象类型,它的所有键都来自联合类型 K,并且这些键对应的值都是同一个类型 T"。
type MyType = Record<'x' | 'y', number>
// 结果:{ x: number; y: number }
// 它等价于你手动写:
type MyType = {
x: number;
y: number;
}
实现自定义映射类型
你可以自己编写映射类型,对属性进行转换:
ts
// 将所有属性变为可选且只读
type ReadonlyPartial<T> = {
readonly [P in keyof T]?: T[P];
};
// 将属性的类型变为布尔值
type Booleanify<T> = {
[P in keyof T]: boolean;
};
使用 as 子句进行键重映射(TypeScript 4.1+)
通过 as 可以过滤或修改键名:
ts
// 过滤掉以 "private" 开头的属性
// [P in keyof T]:遍历 T 的所有键 P
// as ...: 重映射键;决定这个属性最终叫什么名字(或者是否保留)
// P extends `private${string}` ? never : P: 条件判断;如果 P 以 "private" 开头,则映射为 never(表示这个属性被过滤掉);否则映射为原键名 P
// : T[P]: 值的类型保持不变
type RemovePrivateFields<T> = {
[P in keyof T as P extends `private${string}` ? never : P]: T[P];
};
// 使用
type User = {
name: string;
privateAge: number;
privateToken: string;
email: string;
};
type PublicUser = RemovePrivateFields<User>;
// 结果:{ name: string; email: string }
- never 在映射类型中会删除该属性。
- 模板字面量类型
private${string}匹配任何以"private"开头的字符串。
为每个属性生成一个 getter 方法
把一个对象类型 T 转换为另一个对象类型:
原每个属性 P: T[P] 变成 getter 方法 getX(): T[P],其中 X 是 P 首字母大写后的形式。
ts
// 添加 getter 前缀
// [P in keyof T] 遍历 T 的所有键 P
// as `get${Capitalize<string & P>}`: 重映射键名; 拼接 "get" + 首字母大写的原键名
// (): T[P] 值的类型变成一个返回 T[P] 的函数(无参数)
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
// 使用
type Person = {
name: string;
age: number;
};
type PersonGetters = Getters<Person>;
// 结果:
// {
// getName: () => string;
// getAge: () => number;
// }
条件类型
TypeScript 中的条件类型(Conditional Types)是一种根据类型之间的关系进行动态类型选择的工具.
条件类型的基本概念
语法
ts
T extends U ? X : Y
它的含义是:如果类型 T 可以赋值给类型 U(即 T 是 U 的子类型),则结果类型为 X,否则为 Y。
什么叫"子类型"?
把类型想象成集合:
- string 是一个大集合,包含所有可能的字符串。
- "hello" 是这个大集合里的一个小集合(只有一个元素 "hello")。
小的、更具体的集合(子集)可以安全地交给需要大集合的地方。
所以 子类型 = 更精细的类型 ,它是父类型的一个"子集"。
在 TypeScript 里,字面量 "hello" 是 string 的子类型,number 不是 string 的子类型。
例子
ts
type IsNumber<T> = T extends number ? 'yes' : 'no';
type A = IsNumber<42>; // 'yes'
type B = IsNumber<'str'>; // 'no'
42 是 number 的字面量子类型,满足 extends number;而 'str' 不满足,结果为 'no'。
分布式条件类型
当条件类型的 待检查类型 T 是一个裸类型参数(naked type parameter)且是联合类型 时,TypeScript 会将条件类型分布 到联合类型的每个成员上,然后将结果再次合并为联合类型。这一特性称为 分布式条件类型。
触发条件
- T 必须是一个裸类型参数(即直接写在 extends 左边,没有被方括号、数组等包裹,如 T[]、[T]、Promise )。
- T 必须是联合类型(至少两个成员)。
示例
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 包装成元组或数组:
ts
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDistributive<string | number>; // (string | number)[]
因为 [string | number] 不再是一个裸类型参数,TypeScript 会把它当作一个整体,不会分布。
infer 关键字:在条件类型中推断类型
infer 允许你在 extends 子句中声明一个待推断的类型变量,并在 true 分支中使用它。通常用于提取或拆解复杂类型。
语法:
ts
type MyType<T> = T extends SomePattern<infer R> ? R : Fallback;
逐步拆解
- 如果类型 T 匹配模式
SomePattern<...>(SomePattern 是一个泛型类型,例如 Promise、Array 等),则提取出其中的类型参数 R,否则返回 Fallback 类型。 - infer 只能在条件类型的 extends 子句中出现,表示"这里有一个类型参数,请 TypeScript 帮我推断出来"。
SomePattern<infer R>表示:SomePattern 这个泛型类型接受一个参数,我们要把这个参数捕获到类型变量 R 中。
使用:
提取 Promise 内部的类型
ts
// 假设 SomePattern = Promise
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number (Fallback = T)
这里模式是 Promise<infer R>,R 被推断为 string,所以返回 R(即 string)。如果不匹配,返回 T 本身。
提取数组元素类型
ts
type ExtractArrayItem<T> = T extends Array<infer R> ? R : never;
type StrArr = ExtractArrayItem<string[]>; // string
type Num = ExtractArrayItem<number>; // never (Fallback)
自定义泛型模式
ts
type Response<T> = { data: T; status: number };
type ExtractData<T> = T extends Response<infer D> ? D : unknown;
type ApiRes = Response<{ id: number }>;
type DataType = ExtractData<ApiRes>; // { id: number }
type UnknownFallback = ExtractData<string>; // unknown
提取函数返回值类型
ts
// 条件类型 T extends ... ? ... : ... 检查传入的类型 T 是否满足"是一个函数类型"这个条件。
// 模式匹配 (...args: any[]) => infer R 一个接受任意参数、返回类型未知的函数。
// => infer R 表示:函数的返回值类型我们不知道,请 TypeScript 自动推断出来,并命名为 R。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (x: number) => string;
type R = ReturnType<Fn>; // string
工具类型(Utility Types)
TypeScript 内置的工具类型(Utility Types),是基于泛型提供了常见的类型转换能力。
Exclude<T, U>
从联合类型 T 中 剔除 可以赋值给 U 的所有成员。
语法:
ts
type Exclude<T, U> = T extends U ? never : T;
工作原理:
- T 是一个联合类型(或任意类型)
- U 是要排除的类型或条件
- 由于 T 是"裸"类型参数,如果 T 是联合类型,TypeScript 会将 T 的每个成员分别代入 T extends U 判断:
- 如果某个成员可以赋值给 U,则返回 never(表示该成员被丢弃)
- 否则保留该成员
- 最终结果是一个新的联合类型,其中不包含那些可分配给 U 的成员
使用:
ts
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'c'>; // 'b'
// 执行过程:
// 'a' extends 'a'|'c' ? never : 'a' → never
// 'b' extends 'a'|'c' ? never : 'b' → 'b'
// 'c' extends 'a'|'c' ? never : 'c' → never
// 结果:never | 'b' | never → 'b'。
Extract<T, U>
从联合类型 T 中 提取 可以赋值给 U 的所有成员(与 Exclude 相反)。
语法:
ts
type Extract<T, U> = T extends U ? T : never;
工作原理:
- T 和 U 是任意类型,通常 T 为联合类型。
- 同样是分布式条件类型 :将联合类型 T 的每个成员分别代入 T extends U 判断。
- 如果某个成员可以赋值给 U,则保留该成员;
- 否则返回 never(丢弃)。
- 最终结果是一个新的联合类型,只包含那些可分配给 U 的成员(即交集)。
使用:
ts
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'c'>; // 'a' | 'c'
// 执行过程:
// 'a' extends 'a'|'c' ? 'a' : never → 'a'
// 'b' extends 'a'|'c' ? 'b' : never → never
// 'c' extends 'a'|'c' ? 'c' : never → 'c'
// 结果:'a' | never | 'c' → 'a' | 'c'
NonNullable
从类型 T 中 剔除 null 和 undefined。
语法:
ts
type NonNullable<T> = T extends null | undefined ? never : T;
工作原理:
- 分布式条件类型:将联合类型 T 的每个成员与 null | undefined 比较。
- 如果成员是 null 或 undefined,映射为 never;
- 否则保留原成员。
- 最终结果是从 T 中剔除 null 和 undefined 后的类型。
使用:
ts
type T3 = NonNullable<string | number | null | undefined>; // string | number
// 执行过程:
// string extends null|undefined ? never : string → string
// number extends null|undefined ? never : number → number
// null extends ... ? never : null → never
// undefined extends ... ? never : undefined → never
// 结果:string | number
ReturnType
获取函数类型 T 的 返回值类型。
语法:
ts
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never;
工作原理:
- T 必须是一个函数类型(约束
(...args: any) => any)。 - 使用条件类型 + infer 关键字:如果 T 符合函数类型,则推断其返回值类型为 R,并返回 R。
- 该工具不是分布式的(因为 T 被约束为单一函数类型,而非联合)。
- 若 T 不是函数类型,TypeScript 会因为约束而报编译错误。
使用:
javascript
function greet(name: string): number {
return name.length;
}
type T4 = ReturnType<typeof greet>; // number
const arrow = (x: boolean) => x ? 'yes' : 'no';
type T4b = ReturnType<typeof arrow>; // string
Parameters
获取函数类型 T 的 参数类型组成的元组。
语法:
ts
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
工作原理:
- T 必须是函数类型。
- 使用 infer P 推断函数的参数列表,并以元组类型形式返回。
- 保留可选参数、剩余参数等修饰符(例如 param?: type 或 ...rest: type[])。
- 非分布式条件类型。
使用:
ts
function log(level: string, message: string, details?: object): void {}
type T5 = Parameters<typeof log>; // [string, string, details?: object]
function sum(...nums: number[]): number {}
type T5b = Parameters<typeof sum>; // [...nums: number[]]
InstanceType
获取构造器类型 T 的 实例类型(即 new 出来的对象类型)。
语法:
ts
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
工作原理:
- T 必须是构造函数类型(具有 new 签名),允许抽象构造函数(abstract new)。
- 条件类型 + infer:如果 T 符合构造函数签名,则推断其实例类型 R(即 new 出来的对象类型)。
- 返回 R。
- 若 T 不符合约束,编译报错。
使用:
ts
class Person {
constructor(public name: string) {}
age = 0;
}
type T6 = InstanceType<typeof Person>; // Person
// 抽象类
abstract class Animal {}
type T6b = InstanceType<typeof Animal>; // Animal(抽象实例类型)
ConstructorParameters
获取构造器类型 T 的 构造函数参数类型组成的元组。
语法:
ts
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
工作原理:
- T 必须是构造函数类型(支持抽象构造)。
- 使用 infer P 推断构造函数的参数列表,以元组形式返回。
- 保留可选参数、剩余参数等修饰符。
- 非分布式条件类型。
使用:
ts
class Container {
constructor(public id: number, public name?: string) {}
}
type T7 = ConstructorParameters<typeof Container>; // [id: number, name?: string]
// 带剩余参数
class Tuple {
constructor(...items: unknown[]) {}
}
type T7b = ConstructorParameters<typeof Tuple>; // [...items: unknown[]]
注意事项
-
分布式条件类型的意外行为
当
Exclude, Extract, NonNullable的 T`` 参数为联合类型时,它们会逐一处理每个成员。但如果你直接传入一个包含never的类型或any时需要特别小心。例如Exclude<any, string>的结果是never?实际上any extends string会触发分支,但any是特殊的,最终结果可能是any?需要测试:any extends string ? never : any→ 因为条件类型对any会同时匹配两个分支(默认会取联合结果),实际上返回any。但建议避免直接对any使用。 -
函数重载问题
ReturnType, Parameters, InstanceType, ConstructorParameters都只针对最后一个重载签名。如果你的函数或构造函数有多个重载声明,并且最后一个不是你想要的,请使用更精确的类型提取方式(例如手动提取特定签名的类型)。 -
泛型函数与参数
当使用
ReturnType<typeof genericFn>时,结果中可能会保留泛型类型参数,从而造成类型不具体。例如:
ts
function identity<T>(x: T): T { return x; }
type IdReturn = ReturnType<typeof identity>; // 类型为 T(未绑定的类型参数)
这通常不是错误,但在一些需要具体类型的上下文中会导致问题。
never的处理
Exclude<never, anything>的结果是never(因为联合类型没有成员,分布式条件不产生任何结果)。Extract<never, any>也是never。NonNullable<never>是never。
unknown与any的区别
如果用Exclude<unknown, string>结果是什么?unknown extends string ? never : unknown结果为unknown,因为unknown只能赋值给unknown 和 any,不能赋值给string,所以保留unknown。
索引访问类型 (T[K])
TypeScript 中的 索引访问类型(Indexed Access Types)允许你通过键(Key)来获取某个类型(Type)中属性的类型。语法为 T[K],其中 T 是一个类型,K 是一个键(或键的联合类型)。
ts
type Person = {
name: string;
age: number;
address: string;
};
// 索引访问
type T1 = Person['name']; // string
type T2 = Person['age']; // number
type T3 = Person['address']; // string
可以同时传入多个键(联合类型):
ts
type T4 = Person['name' | 'age']; // string | number
与 keyof 结合
keyof T 会返回所有合法键的联合类型,因此 T[keyof T] 能获取所有属性类型的联合:
ts
type PersonKeys = keyof Person; // 'name' | 'age' | 'address'
type AllProps = Person[PersonKeys]; // string | number (因为 name 和 address 是 string, age 是 number)
嵌套访问
索引访问可以链式使用,获取深层属性的类型:
ts
type Company = {
name: string;
address: {
street: string;
city: string;
};
};
type StreetType = Company['address']['street']; // string
结合泛型与条件类型
索引访问在泛型中非常有用,可以用来"提取"某个键对应的类型:
ts
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const p: Person = { name: '张三', age: 30, address: '北京' };
const nameValue = getProperty(p, 'name'); // 类型为 string
注意事项
- 键必须实际存在于类型上,否则 TS 会报错。
- 不能使用 T[K] 来获取变量/值的运行时属性 ------ 它是纯类型层面的操作。
- 当 K 是 string、number 或 symbol 字面量类型时,返回对应属性的类型;如果 K 是更宽泛的类型(如 string),则会应用索引签名或返回联合类型。