文章目录

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 文件为例

// 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 文件,内容如下:
// 这是编译后的纯 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,在此文件夹进行安装

npm install -g typescript

在此文件夹ts下新建一个文件,hello.ts

console.log('Hello world!');

ts文件终端运行 typescript 安装包自带的 tsc 指令进行类型检查。

tsc hello.ts

在文件夹下能看到多出了 hello.js 文件,这是 tsc 编译或者转换 hello.ts 文件之后输出的纯 JavaScript 文件。

基础类型

原始类型: string, number, boolean

类型注解(Type Annotation):在 TypeScript 中,使用 变量名: 类型 的语法来为变量、函数参数、函数返回值等添加类型标注。

核心语法:

let/const/var 变量名: 类型 =;
  1. string - 字符串类型

let 变量名: string = ‘值’;
let 变量名: string = 模板字符串 ${表达式};

// 基本声明
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 = 数字;

// 整数和浮点数
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;

// 基本声明
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];

// 方式一:类型[]
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];

//  基本元组
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 = 任意值;

// 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 = 任意值;

// unknown 可以接收任何类型的值
let safer: unknown = "something";
// safer.toUpperCase(); // 错误,不能直接调用
if (typeof safer === "string") {
    safer.toUpperCase(); // 类型检查后可以
}

  1. void - 无返回值类型
    表示函数不返回任何有意义的值,或者变量值为 undefined

function 函数名(): void { }
let 变量名: void = undefined;

// 不返回值的函数
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 本身是类型
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 - 永不存在的值
// 抛出异常或无限循环的函数
function error(message: string): never {
  throw new Error(message);
}

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

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

  1. 联合类型 (|)
    联合类型表示值可以是多种类型中的一种,使用竖线 | 分隔。就是或,多选一
// 变量可以是 string 或 number 类型
let id: string | number;
id = 'abc123'; 
id = 456; 
// id = true;   //  错误
  1. 字面量类型
    字面量类型是指将具体的值作为类型使用,包括字符串、数字、布尔值等。
// 字符串字面量类型
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. 交叉类型 (&)
    交叉类型将多个类型合并为一个类型,包含所有类型的特性。
    接口和类型后面会讲到,看不懂可以先跳过。后面会再讲
// 合并两个接口
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 能根据初始化值自动推断类型,无需显式注解。

// 自动推断为 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 类型

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

<类型>值

// 传统形式
let someValue: unknown = "TypeScript";
let strLength: number = (<string>someValue).length;

// ⚠️ 注意:在 TSX 中会与 JSX 语法冲突
// 以下代码在 .tsx 文件中会报错
const element = <string>someValue;  // 会被解析为 JSX 标签

场景

// 场景:从 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. 语法

function 函数名(参数名: 参数类型): 返回类型 {
    // 函数体
    return 返回值;
}

2. 例子

// 函数
function sayHello(name: string): string {
	return "hello, " + name;
}

// 调用,参数为字符串
sayHello("小明");  // 返回 "hello, 小明"

// 错误使用
sayHello(123);     // 错误:参数类型不匹配
sayHello(true);    // 错误:参数类型不匹配

解释:

  • name: string 表示这个函数必须接收一个字符串类型的参数
  • :string 表示这个函数会返回一个字符串
  • 如果调用时传入非字符串,TypeScript 会在你写代码时就提示错误

3. 常见的返回值基础类型

// 返回值为字符串类型
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. 类型推断

// 不需要写返回类型,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 同样支持类型注解:

// 传统函数写法
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;

可选参数 , 默认参数, 剩余参数

可选参数(?)

场景:有些参数不是必须的,调用者可以选择是否提供。

// 用问号 ? 表示参数是可选的
function introduce(name: string, age?: number): string {
    if (age) {
        return `我叫${name},今年${age}`;
    } else {
        return `我叫${name}`;
    }
}

// 使用方式
introduce("小明");           // ✅ 输出:"我叫小明"
introduce("小明", 18);       // ✅ 输出:"我叫小明,今年18岁"

注:可选参数必须放在必选参数后面

下面这种写法是错误的:

// 可选参数不能出现在必选参数前面
function wrong(age?: number, name: string) { }

默认参数

场景:如果调用者没有提供参数,就使用一个默认值。

// 直接在参数后面用等号赋值
function greet(name: string, greeting: string = "你好"): string {
    return `${greeting}${name}`;
}

// 使用方式
greet("小明");              // ✅ 输出:"你好,小明"(使用了默认值)
greet("小明", "早上好");    // ✅ 输出:"早上好,小明"(覆盖默认值)

默认参数和可选参数的区别:

  • 可选参数:不提供就是 undefined
  • 默认参数:不提供就使用你设定的值
// 可选参数 vs 默认参数
function test1(name?: string): void {
    console.log(name);  // 不传参时输出 undefined
}

function test2(name: string = "匿名"): void {
    console.log(name);  // 不传参时输出 "匿名"
}

test1();  // undefined
test2();  // "匿名"

剩余参数(…)

场景:函数需要接收任意数量的参数。

// 使用 ... 把所有剩余参数收集到一个数组中
// 剩余参数必须放在最后
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[](数字数组)
  • 剩余参数必须放在最后

找出最大值

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)

什么是函数重载?为什么要用重载?

**函数重载:**允许你为同一个函数定义多个不同的类型签名,根据不同的参数类型或数量返回不同的结果。
问题场景:有时候一个函数需要根据传入参数的类型或数量,返回不同类型的结果。

// 这个函数想要实现:
// - 传入一个数字,返回一个数字
// - 传入一个字符串,返回一个字符串
// - 传入一个数组,返回一个数组

function process(value) {
    // 根据 value 的类型执行不同的逻辑
}

重载的写法

重载分为两部分:

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

为什么不用联合类型?

// 如果这样写(没有重载)
function process(value: string | number | boolean): string | number | boolean {
    // 实现相同...
}

let result = process("hello");
// result 的类型是 string | number | boolean(联合类型)
// TypeScript 不确定具体是哪个,你需要手动判断
result.toUpperCase();  // 报错!TypeScript 认为可能不是字符串

使用重载后,TypeScript 能准确知道返回类型。

参数数量不同的重载

// 重载签名
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岁"

泛型函数

为什么需要泛型?

问题场景:你想写一个"通用的"函数,可以处理多种类型,但又要保持类型安全。
不使用泛型的困境:

// 方案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;
}
// 输入字符串,返回的可能是数字,类型信息丢失了

泛型的基本用法

泛型就像是"类型的变量",用尖括号 表示:

// 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

多个泛型参数

// 接收两个不同类型的参数,返回一个数组(元组)
// <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

泛型约束(限制泛型的范围)

问题:有时候你希望泛型必须有某些属性。

// 没有约束的版本
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. 获取数组中第一个元素
// <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 请求
// 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)的结合使用,是对象合并的经典模式。
// <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 代码中。

基础语法

// 基本语法结构
interface 接口名 {
    属性名: 类型;
    属性名: 类型;
    // ...
}

// 示例:定义用户接口
interface User {
    id: number;
    name: string;
    age: number;
}

// 使用接口
const userInfo: User = {
    id: 1,
    name: "Alice",
    age: 25
};

可选属性(Optional Properties)
在实际开发中,很多对象的属性并不是必需的。比如用户信息,可能只有 name 是必需的,age、email 等是可选的。

// 语法结构
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、创建时间戳等。

// 语法结构
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

interface Data {
  values: readonly number[];  // 只读数组 - 不能修改数组内容
  // 对象递归只读,等价于:{ readonly apiKey: string;}
  config: Readonly<{ apiKey: string }>; 
}

索引签名(Index Signatures)
当你不确定对象会有哪些属性名,但知道属性值的类型时,索引签名就非常有用。

// 场景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(类型别名)

核心概念
类型别名是为任何类型创建的一个新名字。相比接口只能描述对象/函数,类型别名可以描述所有类型,包括原始类型、联合类型、元组等。

基本语法与使用场景

// 基本语法
type 类型别名 = 类型;
let 变量名: 类型别名 =// 示例
type Name = string;

// 使用
let username: Name = "Alice";
  1. 联合类型(Union Types):一个值可能是多种类型之一
// 基础联合
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):固定长度、固定顺序、每个位置类型可不同
// 基础元组
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): 合并多个类型
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. 对象类型
// 定义对象结构
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. 函数类型
// 定义函数类型
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. 泛型类型别名
// 泛型类型
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. 其他
// 字面量
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 的返回值类型

// 这是一个泛型元组类型
// 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)

核心思想:创建一个更具体的子接口,继承(复制)父接口的所有成员。

核心语法

// 基础语法
// 使用关键字extends,继承了的子接口拥有父接口所有属性和方法
interface ChildInterface extends ParentInterface {
    // 子接口自己的成员
    newProperty: string;
}

基础继承

// 基础继承
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("汪汪");
  }
};

多接口继承:

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; }  // 同时满足两个接口的要求
};

类型交织 (&)

核心思想:将多个类型合并成一个新类型

// 基础交织
type Animal = {
  name: string;
  eat(): void;
};

type Dog = Animal & {
  breed: string;
  bark(): void;
};

// 和继承达到类似效果
const myDog: Dog = {
  name: "旺财",
  breed: "金毛",
  eat() {},
  bark() {}
};

复杂类型的交织:

// 联合类型的交织
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. 同名属性处理
// 接口继承:子接口的同名属性必须兼容父接口
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. 声明合并能力
// 接口支持声明合并
interface User { name: string; }
interface User { age: number; }
// 最终 User 类型同时有 name 和 age

// 类型不支持重复声明
type User { name: string; }
type User { age: number; }  // 错误:Duplicate identifier 'User'
  1. 递归引用
// 两者都支持递归引用
interface TreeNode {
  value: string;
  children?: TreeNode[];  // 接口可以递归
}

type Tree = {
  value: string;
  children?: Tree[];  // 类型别名也可以递归
};

// 但类型别名在更复杂的情况下可能有问题
type List = number | List[];  // 某些旧版本可能有警告

实际项目中的选择策略

使用 interface 的场景

// 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 的场景

// 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 的。
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 修饰的成员只能在声明它的类内部被访问。即使是该类的子类也无法访问父类的私有成员。这是最严格的访问控制级别。
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 的 # 私有字段:

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 之间。
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 组合使用
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 用于实例属性,可以在运行时确定值,每个实例可以不同
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 修饰的成员**属于类本身而不是类的实例。**静态成员通过类名直接访问,不需要创建实例。所有实例共享同一个静态成员。
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)
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() 调用)
// 定义抽象类
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 关键字用于让一个类遵循某个接口的契约。接口定义了类应该具有的成员(属性、方法),类必须实现接口中声明的所有成员。

核心要点:

  • 一个类可以实现多个接口
  • 接口只定义结构,不提供实现
  • 类可以实现多个接口,用逗号分隔
  • 类在实现接口的同时还可以有自己的额外成员
  • 接口可以继承其他接口

类实现单个接口

// 定义接口
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;
    }
}

类实现多个接口

// 定义接口
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. 接口继承与类实现
// 接口可以继承其他接口
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. 接口与类的区别
// 接口定义可选属性、只读属性
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
// 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 会自动创建同名属性并赋值。
// 传统写法 - 冗长
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. 参数属性的高级用法
  • 混合使用参数属性和普通属性
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;
    }
}
  • 参数属性与默认值
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"
  • 参数属性与可选参数
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;
    }
}
  • 参数属性与解构(高级技巧)
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" };
    }
}
  • 参数属性与依赖注入(常见模式)
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);

泛型

上面函数里有提到过,现在复习一下及系统地了解。
**泛型允许我们在定义接口或类时,不预先指定具体类型,而是留到使用时再确定。**这样可以提高代码的复用性和类型安全。
一句话:不预先固定类型,而是在使用时才确定类型。

语法

// <T> 就像一个“类型的占位符”,调用时才决定它具体是什么类型。
// value: T 说:参数的类型就是那个占位符。
// : T 说:返回的类型也和参数一样。
function identity<T>(value: T): T {
  return value
}
// 调用时:
let a = identity(123)      // 类型为 number
let b = identity("hello")  // 类型为 string
// 效果:一个函数,适配所有类型,同时保留精确的类型信息。

函数泛型

// 尖括号在函数名后,参数列表前
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;

接口泛型

// 接口名后跟 <类型参数>
interface Box<T> {
  value: T;
  get(): T;
}

// 使用
let box: Box<string> = { value: "hi", get() { return this.value; } };

类型别名泛型

// 类型别名后跟 <类型参数>
type Result<T> = { ok: true; value: T } | { ok: false; error: string };

// 使用
let res: Result<number> = { ok: true, value: 42 };

类泛型

// 类名后跟 <类型参数>
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. 约束属性/方法
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. 约束构造函数(可实例化的类型)
// 约束 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. 约束类型参数之间的关系
    一个泛型参数可以约束另一个:
// 确保 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 与约束结合
// 将对象的某个属性映射为其他值
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. 基本语法与场景
    多个类型参数用逗号分隔,通常用于需要同时处理多种不同类型的情况。
  • 交换元组中的类型
function swap<T, U>(pair: [T, U]): [U, T] {
  return [pair[1], pair[0]];
}

const swapped = swap([1, "hello"]); // 类型为 [string, number],实际值为 ["hello", 1]
  • 键值映射函数
// 将数组中的每个元素转换为键值对
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。
// 复制对象的指定属性到新对象
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. 高级模式:柯里化与类型安全
// 柯里化函数,逐步固化类型
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. 接口中的默认泛型
// 默认类型为 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. 类中的默认泛型
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 会根据参数自动推断,默认值往往被覆盖。
// 默认泛型为 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?实际很少用到

更实用的函数默认泛型场景:高阶函数,比如创建一个“配置对象生成器”。

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 约束类型 = 默认类型>
// 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 会报错)。

泛型参数的作用域与嵌套

// 外部泛型参数可以在内部使用
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];
}

泛型参数用于高阶类型

// 泛型参数可以传给其他泛型
type Wrapped<T> = { data: T };
type DoublyWrapped<T> = Wrapped<Wrapped<T>>;

// 泛型参数也可以用于条件类型
type IsArray<T> = T extends any[] ? true : false;

typeof 与泛型的混合语法

// 从变量推导类型,并用作泛型参数
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 组件中

// 函数组件(.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 会根据比较结果缩窄变量类型。

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,则将变量类型缩窄为该构造函数的实例类型。

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 会认为该变量具有该属性,从而将其缩窄到包含该属性的类型(通常是联合类型中的某一成员)。

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 后面的类型。
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 判断。
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)。

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 可以是任意类型)
type Keys = 'name' | 'age';
type Person = {
  [K in Keys]: string | number;
};
// 等价于 { name: string | number; age: string | number; }

常用内置映射类型
TypeScript 提供了多个实用的内置映射类型,避免手写的繁琐

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;
}

实现自定义映射类型
你可以自己编写映射类型,对属性进行转换:

// 将所有属性变为可选且只读
type ReadonlyPartial<T> = {
  readonly [P in keyof T]?: T[P];
};

// 将属性的类型变为布尔值
type Booleanify<T> = {
  [P in keyof T]: boolean;
};

使用 as 子句进行键重映射(TypeScript 4.1+)
通过 as 可以过滤或修改键名:

// 过滤掉以 "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 首字母大写后的形式。

// 添加 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)是一种根据类型之间的关系进行动态类型选择的工具.

条件类型的基本概念

语法

T extends U ? X : Y

它的含义是:如果类型 T 可以赋值给类型 U(即 T 是 U 的子类型),则结果类型为 X,否则为 Y。

什么叫“子类型”?
把类型想象成集合:

  • string 是一个大集合,包含所有可能的字符串。
  • “hello” 是这个大集合里的一个小集合(只有一个元素 “hello”)。
    小的、更具体的集合(子集)可以安全地交给需要大集合的地方。
    所以 子类型 = 更精细的类型,它是父类型的一个“子集”。
    在 TypeScript 里,字面量 “hello” 是 string 的子类型,number 不是 string 的子类型。

例子

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 必须是联合类型(至少两个成员)。

示例

type ToArray<T> = T extends any ? T[] : never;

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

如果不希望发生分布式行为,可以将 T 包装成元组或数组

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

因为 [string | number] 不再是一个裸类型参数,TypeScript 会把它当作一个整体,不会分布。

infer 关键字:在条件类型中推断类型

infer 允许你在 extends 子句中声明一个待推断的类型变量,并在 true 分支中使用它。通常用于提取或拆解复杂类型。

语法:

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 内部的类型

// 假设 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 本身。

提取数组元素类型

type ExtractArrayItem<T> = T extends Array<infer R> ? R : never;

type StrArr = ExtractArrayItem<string[]>;   // string
type Num = ExtractArrayItem<number>;        // never (Fallback)

自定义泛型模式

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

提取函数返回值类型

// 条件类型 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 的所有成员。

语法:

type Exclude<T, U> = T extends U ? never : T;

工作原理:

  • T 是一个联合类型(或任意类型)
  • U 是要排除的类型或条件
  • 由于 T 是“裸”类型参数,如果 T 是联合类型,TypeScript 会将 T 的每个成员分别代入 T extends U 判断:
    • 如果某个成员可以赋值给 U,则返回 never(表示该成员被丢弃)
    • 否则保留该成员
  • 最终结果是一个新的联合类型,其中不包含那些可分配给 U 的成员

使用:

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 相反)。

语法:

type Extract<T, U> = T extends U ? T : never;

工作原理:

  • T 和 U 是任意类型,通常 T 为联合类型。
  • 同样是分布式条件类型:将联合类型 T 的每个成员分别代入 T extends U 判断。
    • 如果某个成员可以赋值给 U,则保留该成员;
    • 否则返回 never(丢弃)。
  • 最终结果是一个新的联合类型,只包含那些可分配给 U 的成员(即交集)。

使用:

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。

语法:

type NonNullable<T> = T extends null | undefined ? never : T;

工作原理:

  • 分布式条件类型:将联合类型 T 的每个成员与 null | undefined 比较。
    • 如果成员是 null 或 undefined,映射为 never;
    • 否则保留原成员。
  • 最终结果是从 T 中剔除 null 和 undefined 后的类型。

使用:

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 的 返回值类型

语法:

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 会因为约束而报编译错误。

使用:

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 的 参数类型组成的元组

语法:

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

工作原理:

  • T 必须是函数类型。
  • 使用 infer P 推断函数的参数列表,并以元组类型形式返回。
  • 保留可选参数、剩余参数等修饰符(例如 param?: type 或 …rest: type[])。
  • 非分布式条件类型。

使用:

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 出来的对象类型)。

语法:

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 不符合约束,编译报错。

使用:

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 的 构造函数参数类型组成的元组

语法:

type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

工作原理:

  • T 必须是构造函数类型(支持抽象构造)。
  • 使用 infer P 推断构造函数的参数列表,以元组形式返回。
  • 保留可选参数、剩余参数等修饰符。
  • 非分布式条件类型。

使用:

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> 时,结果中可能会保留泛型类型参数,从而造成类型不具体。例如:

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 是一个键(或键的联合类型)。

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

// 索引访问
type T1 = Person['name']; // string
type T2 = Person['age'];  // number
type T3 = Person['address']; // string

可以同时传入多个键(联合类型):

type T4 = Person['name' | 'age']; // string | number

与 keyof 结合
keyof T 会返回所有合法键的联合类型,因此 T[keyof T] 能获取所有属性类型的联合:

type PersonKeys = keyof Person; // 'name' | 'age' | 'address'
type AllProps = Person[PersonKeys]; // string | number (因为 name 和 address 是 string, age 是 number)

嵌套访问
索引访问可以链式使用,获取深层属性的类型:

type Company = {
  name: string;
  address: {
    street: string;
    city: string;
  };
};

type StreetType = Company['address']['street']; // string

结合泛型与条件类型
索引访问在泛型中非常有用,可以用来“提取”某个键对应的类型:

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),则会应用索引签名或返回联合类型。

更多推荐