TypeScript

文章目录

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)" 完全分离的机制。

  1. 编写阶段:你写的是 .ts 文件,里面有类型。
  2. 编译阶段 :运行 tsc 命令。tsc 会:
    • 解析你的 .ts 文件。
    • 进行全面的类型检查,如果发现类型错误,会报错并停止编译。
    • 擦除所有类型代码,生成纯 .js 文件。
  3. 执行阶段:运行 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)。编译器会做两件事:

  1. 检查:确认 name 和 userName 确实是 string 类型。
  2. 编译:生成 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 变量名: 类型 = 值;
  1. 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"
  1. 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
  1. 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

  1. 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("动态方法");
  1. unknown
    表示任何值,但比 any 更严格,强制进行类型检查。

let 变量名: unknown = 任意值;

ts 复制代码
// unknown 可以接收任何类型的值
let safer: unknown = "something";
// safer.toUpperCase(); // 错误,不能直接调用
if (typeof safer === "string") {
    safer.toUpperCase(); // 类型检查后可以
}
  1. 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);
    };
}
  1. 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;
  1. never - 永不存在的值
ts 复制代码
// 抛出异常或无限循环的函数
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

联合类型 (|) , 字面量类型 ,交叉类型 (&)

  1. 联合类型 (|)
    联合类型表示值可以是多种类型中的一种,使用竖线 | 分隔。就是或,多选一
ts 复制代码
// 变量可以是 string 或 number 类型
let id: string | number;
id = 'abc123'; 
id = 456; 
// id = true;   //  错误
  1. 字面量类型
    字面量类型是指将具体的值作为类型使用,包括字符串、数字、布尔值等。
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
  1. 交叉类型 (&)
    交叉类型将多个类型合并为一个类型,包含所有类型的特性。
    接口和类型后面会讲到,看不懂可以先跳过。后面会再讲
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 编译器"相信我,我知道这个值的类型"的方式。它不会改变值的运行时类型,只是在编译阶段影响类型检查。

  1. 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;
  1. 尖括号 <> 语法

<类型>值

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 的类型执行不同的逻辑
}

重载的写法

重载分为两部分:

  1. 重载签名(多个):告诉 TypeScript 这个函数可以怎么调用
  2. 实现签名(一个):真正写函数的逻辑
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 属性

实际的泛型应用例子

  1. 获取数组中第一个元素
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
  1. 类型安全的 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 属性
  1. 合并两个对象
    这个例子展示了泛型与交叉类型(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";
  1. 联合类型(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
  }
}
  1. 元组类型(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];
  1. 交叉类型(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"
};
  1. 对象类型
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 是可选的,可以不提供
};
  1. 函数类型
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
  1. 泛型类型别名
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" }
};
  1. 其他
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 属性

注意事项

  1. 同名属性处理
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 类型没有值
  1. 声明合并能力
ts 复制代码
// 接口支持声明合并
interface User { name: string; }
interface User { age: number; }
// 最终 User 类型同时有 name 和 age

// 类型不支持重复声明
type User { name: string; }
type User { age: number; }  // 错误:Duplicate identifier 'User'
  1. 递归引用
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 提供了三种访问修饰符来控制类成员(属性和方法)的可见性。这是面向对象封装特性的核心体现。

  1. 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);               // 可以调用
  1. 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);  // 语法错误
  1. 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 属性外部不能访问
  1. 三种修饰符对比总结
修饰符 类内部 子类内部 类外部 典型用途
public API接口、对外属性
protected 继承使用的属性、模板方法
private 内部状态、实现细节

特殊修饰符详解 readonly, static, abstract

  1. 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);
    }
}
  1. 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();  // "张" "张"
  1. 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)

  1. 基本概念
    含义: 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;
    }
}
  1. 接口继承与类实现
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;
    }
}
  1. 接口与类的区别
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, "张三");
  1. 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();
    }
}

参数属性

  1. 基本概念与用法
    含义:参数属性是 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 可以被外部读取
  1. 参数属性的高级用法
  • 混合使用参数属性和普通属性
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())。约束可以限定泛型必须满足某个接口。

  1. 约束属性/方法
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 属性
  1. 约束构造函数(可实例化的类型)
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"
  1. 约束类型参数之间的关系
    一个泛型参数可以约束另一个:
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 的键
  1. 使用 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",否则类型错误

多个类型参数

  1. 基本语法与场景
    多个类型参数用逗号分隔,通常用于需要同时处理多种不同类型的情况。
  • 交换元组中的类型
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" }
  1. 类型参数之间的依赖与约束
    后面的参数可以依赖于前面的参数,例如 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 不存在于返回类型中
  1. 高级模式:柯里化与类型安全
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>

  1. 接口中的默认泛型
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",
};
  1. 类中的默认泛型
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 中
  1. 函数中的默认泛型
    注意:函数中的默认泛型通常在类型参数无法推断且未显式指定时生效。但 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 }
  1. 默认类型与约束同时使用
    顺序:<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)变量的类型。

简单说 ,类型守卫 是用于在代码块中缩小类型范围的技术。

  1. 类型守卫 -- 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
  }
}
  1. 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
  }
}
  1. 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
  }
}
  1. 自定义类型守卫(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);
  }
}
  1. 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());
}
  1. 真值缩窄(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[]]

注意事项

  1. 分布式条件类型的意外行为

    Exclude, Extract, NonNullable 的 T`` 参数为联合类型时,它们会逐一处理每个成员。但如果你直接传入一个包含 never 的类型或 any 时需要特别小心。例如 Exclude<any, string> 的结果是 never?实际上 any extends string 会触发分支,但 any 是特殊的,最终结果可能是 any?需要测试:any extends string ? never : any → 因为条件类型对 any 会同时匹配两个分支(默认会取联合结果),实际上返回 any。但建议避免直接对any使用。

  2. 函数重载问题
    ReturnType, Parameters, InstanceType, ConstructorParameters 都只针对最后一个重载签名。如果你的函数或构造函数有多个重载声明,并且最后一个不是你想要的,请使用更精确的类型提取方式(例如手动提取特定签名的类型)。

  3. 泛型函数与参数

    当使用 ReturnType<typeof genericFn> 时,结果中可能会保留泛型类型参数,从而造成类型不具体。例如:

ts 复制代码
function identity<T>(x: T): T { return x; }
type IdReturn = ReturnType<typeof identity>; // 类型为 T(未绑定的类型参数)

这通常不是错误,但在一些需要具体类型的上下文中会导致问题。

  1. never 的处理
  • Exclude<never, anything> 的结果是 never(因为联合类型没有成员,分布式条件不产生任何结果)。
  • Extract<never, any> 也是 never
  • NonNullable<never> never
  1. unknownany 的区别
    如果用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),则会应用索引签名或返回联合类型。
相关推荐
Restart-AHTCM3 小时前
AI时代大前端Agent开发LangChain.js
typescript·langchain·memory·rag·tools
枕星而眠4 小时前
Linux 四大进程/线程同步锁详解:互斥锁、读写锁、条件变量、文件锁
linux·c语言·后端·ubuntu·学习方法
Edwardwu5 小时前
写了个y-mxgraph:给 draw.io 接上了 Yjs,顺便解决了部署在 iframe 里的一堆问题
前端·typescript
熊猫_豆豆6 小时前
一个模拟四轴飞行器在随机气流扰动下悬停飞行的交互式3D仿真网页,包含飞行器建模与PID控制算法
javascript·3d·html·四轴无人机模拟飞行
阿正的梦工坊6 小时前
【Typescript】14-高级实战-设计类型安全的-api
typescript
来恩10037 小时前
jQuery选择器
前端·javascript·jquery
前端繁华如梦7 小时前
树上挂苹果还是挂玻璃球?Three.js 程序化果实的完整实现指南
前端·javascript
CDwenhuohuo7 小时前
优惠券组件直接用 uview plus
前端·javascript·vue.js
川冰ICE8 小时前
TypeScript装饰器与元编程实战
前端·javascript·typescript