第一章 概述与环境搭建

1.1 什么是 TypeScript

TypeScript(简称TS)是微软开发的一款开源编程语言,它是 JavaScript 的超集(Superset)。这意味着 TypeScript 包含了 JavaScript 的所有语法和功能,同时在其基础上增加了静态类型系统

核心目标:通过类型约束提升代码的可靠性、可维护性,帮助开发者在编码阶段提前发现错误,而非运行时;同时增强代码的可读性和可扩展性,尤其适合中大型项目和团队协作。

TypeScript 代码最终会被编译成纯 JavaScript 代码,可运行在任何支持 JavaScript 的环境(浏览器、Node.js 等)。

1.2 TypeScript 与 JavaScript 的区别

特性JavaScriptTypeScript
类型系统动态类型(运行时检查类型)静态类型(编译时检查类型)
类型约束无,变量类型可随时改变有,变量声明时指定类型,不可随意更改
代码运行直接运行需编译为 JS 后运行
适用场景小型项目、快速原型开发中大型项目、团队协作
额外特性接口、泛型、枚举、高级类型等

1.3 环境搭建(核心步骤)

TypeScript 编译依赖 Node.js 环境,需先安装 Node.js(自带 npm 包管理器)。

1.3.1 安装 Node.js

  1. 下载地址:https://nodejs.org/(推荐下载 LTS 长期支持版)

  2. 验证安装:打开终端,输入以下命令,若显示版本号则安装成功


node -v  # 查看 Node.js 版本
npm -v   # 查看 npm 版本

1.3.2 安装 TypeScript 编译器(全局)

终端输入以下命令,安装完成后可通过 tsc 命令调用编译器


npm install -g typescript

验证安装:


tsc -v  # 显示 TypeScript 版本,如 Version 5.3.3

1.3.3 第一个 TypeScript 程序

  1. 创建项目文件夹,新建文件 hello.ts(TS 文件后缀为 .ts)

  2. 编写代码:

// hello.ts
const message: string = "Hello TypeScript!";
console.log(message);
  1. 编译 TS 文件:终端进入项目文件夹,输入以下命令,会生成对应的 hello.js 文件

tsc hello.ts
  1. 运行 JS 文件:
node hello.js  # 输出:Hello TypeScript!

1.3.4 配置文件(tsconfig.json)

当项目较大时,可通过配置文件指定编译规则,终端输入以下命令生成 tsconfig.json


tsc --init

核心配置项说明(常用):

{
  "compilerOptions": {
    "target": "ES6",                          /* 编译后的 JS 版本(ES3/ES5/ES6等) */
    "module": "CommonJS",                     /* 模块系统(CommonJS/ESNext等) */
    "outDir": "./dist",                       /* 编译后的 JS 文件输出目录 */
    "rootDir": "./src",                       /* 源代码(TS 文件)目录 */
    "strict": true,                           /* 开启严格模式(强烈推荐,严格检查类型) */
    "esModuleInterop": true,                  /* 解决 ES 模块与 CommonJS 模块兼容问题 */
    "skipLibCheck": true,                     /* 跳过第三方库的类型检查 */
    "forceConsistentCasingInFileNames": true  /* 强制文件名大小写一致 */
  },
  "include": ["./src/**/*"],                  /* 需要编译的文件(src 下所有文件及子目录) */
  "exclude": ["node_modules"]                 /* 排除不需要编译的目录 */
}

配置完成后,直接输入 tsc 命令,会自动按照配置编译 rootDir 下的所有 TS 文件,输出到 outDir 目录。

第二章 基础类型(核心重点)

TypeScript 提供了多种基础类型,用于约束变量的类型。声明变量时通过 变量名: 类型 的格式指定类型,赋值时类型必须匹配(严格模式下)。

2.1 布尔值(boolean)

表示真或假,只有两个值:truefalse

// 正确声明
const isDone: boolean = true;
const isLoading: boolean = false;

// 错误:赋值类型不匹配(严格模式下报错)
// const isSuccess: boolean = 1;  // 类型“number”不能赋值给类型“boolean”

2.2 数字(number)

包含整数、浮点数、十六进制、十进制、二进制、八进制等,与 JavaScript 一致。


const age: number = 18;          // 整数
const price: number = 99.9;      // 浮点数
const hex: number = 0xff00;      // 十六进制(255*256 = 65280)
const binary: number = 0b1010;   // 二进制(10)
const octal: number = 0o744;     // 八进制(484)
const infinity: number = Infinity;  // 无穷大
const nan: number = NaN;          // 非数字

2.3 字符串(string)

表示文本数据,可用单引号、双引号或模板字符串(反引号 ``)。


const name: string = '张三';
const message: string = "Hello TS";
// 模板字符串:支持换行和变量插值
const info: string = `我的名字是${name},年龄是${age}`;
console.log(info);  // 输出:我的名字是张三,年龄是18

2.4 数组(Array)

有两种声明方式:

  1. 类型[]:简洁常用

  2. Array<类型>:泛型方式(后续详解泛型)


// 方式1:类型[]
const numbers: number[] = [1, 2, 3, 4];
const names: string[] = ['张三', '李四', '王五'];

// 方式2:Array<类型>
const booleans: Array<boolean> = [true, false, true];

// 错误:数组元素类型不匹配
// const arr: number[] = [1, '2', 3];  // 类型“string”不能赋值给类型“number”

// 空数组(需指定类型,否则为 never[])
const emptyArr: string[] = [];

2.5 元组(Tuple)

元组是固定长度、固定类型顺序的数组,各元素类型可以不同。


// 声明一个元组:第一个元素string,第二个元素number
const user: [string, number] = ['张三', 18];

// 访问元组元素(类型明确)
const userName: string = user[0];
const userAge: number = user[1];

// 错误1:长度不匹配
// const user2: [string, number] = ['李四'];  // 源具有 1 个元素,但目标需要 2 个元素

// 错误2:类型顺序不匹配
// const user3: [string, number] = [20, '王五'];  // 类型“number”不能赋值给类型“string”

// 元组越界访问(不推荐,严格模式下可能报错)
// user.push(20);  // 可以添加,但访问时类型为联合类型 string | number
// console.log(user[2]);  // 20,但类型检查不严格

2.6 枚举(enum)

枚举用于定义一组命名的常量,解决“魔法数字”问题,增强代码可读性。默认情况下,枚举值从 0 开始递增,也可手动指定值。

语法:enum 枚举名 { 常量1, 常量2, ... }

2.6.1 数字枚举(默认)


// 默认枚举:值从 0 开始递增
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right  // 3
}

// 使用枚举
const currentDir: Direction = Direction.Up;
console.log(currentDir);  // 输出:0

// 手动指定枚举值
enum Direction2 {
  Up = 10,  // 10
  Down,     // 11(自动递增)
  Left = 20, // 20
  Right     // 21(自动递增)
}
console.log(Direction2.Down);  // 11
console.log(Direction2.Right); // 21

2.6.2 字符串枚举

所有枚举值必须手动指定字符串,不支持自动递增。


enum Status {
  Success = 'SUCCESS',
  Fail = 'FAIL',
  Pending = 'PENDING'
}

const requestStatus: Status = Status.Success;
console.log(requestStatus);  // 输出:SUCCESS

2.6.3 异构枚举(不推荐)

枚举值同时包含数字和字符串(混合使用易混乱,尽量避免)。


enum MixEnum {
  A = 1,
  B = 'B',
  C = 2
}
console.log(MixEnum.A);  // 1
console.log(MixEnum.B);  // B

2.6.4 枚举的反向映射

数字枚举支持反向映射(通过值获取枚举名),字符串枚举不支持。


enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right  // 3
}

// 正向映射:枚举名 -> 值
console.log(Direction.Up);  // 0

// 反向映射:值 -> 枚举名
console.log(Direction[0]);  // Up

2.7 任意类型(any)

当变量类型不确定(如接收用户输入、第三方库数据)时,可指定为 any,此时 TypeScript 会关闭类型检查,变量可赋值为任意类型,也可调用任意方法(即使不存在)。

注意:尽量少用 any,否则会失去 TypeScript 类型检查的意义,可用 unknown(后续详解)替代。


let anyVar: any = 123;
anyVar = 'hello';  // 可以赋值为字符串
anyVar = true;     // 可以赋值为布尔值
anyVar = { name: '张三' };  // 可以赋值为对象

// 调用任意方法(即使不存在,也不报错)
anyVar.sayHello();  // 运行时可能报错,但编译时不检查

2.8 未知类型(unknown)

unknownany类似,可接收任意类型的值,但 unknown 更严格:不能直接赋值给其他类型变量,也不能直接调用方法,必须通过类型断言或类型守卫确认类型后才能使用。


let unknownVar: unknown = 123;
unknownVar = 'hello';
unknownVar = true;

// 错误1:不能直接赋值给其他类型
// const str: string = unknownVar;  // 类型“unknown”不能赋值给类型“string”

// 错误2:不能直接调用方法
// unknownVar.toUpperCase();  // 类型“unknown”上不存在属性“toUpperCase”

// 正确用法:通过类型断言确认类型
const str1: string = unknownVar as string;  // 类型断言(推荐)
const str2: string = <string>unknownVar;  // 另一种类型断言方式(不推荐,与JSX冲突)
str1.toUpperCase();  // 正确

// 正确用法:通过类型守卫确认类型
if (typeof unknownVar === 'string') {
  unknownVar.toUpperCase();  // 此时 unknownVar 被推断为 string 类型
}

2.9 空值(void)

void 表示函数没有返回值(即返回 undefined 或不写 return)。变量声明为 void时,只能赋值为 undefinednull(非严格模式下)。


// 函数返回 void(无返回值)
function logMessage(): void {
  console.log('这是一条日志');
  // 可省略 return,或 return undefined
  // return;
  // return undefined;
}

// 错误:函数返回值类型不匹配
// function add(a: number, b: number): void {
//   return a + b;  // 类型“number”不能赋值给类型“void”
// }

// 变量声明为 void(意义不大,很少用)
const voidVar: void = undefined;

2.10 永不存在的值(never)

never 表示永远不会发生的值,常用于以下场景:

  1. 函数抛出异常(永远不会正常返回)

  2. 函数进入无限循环(永远不会返回)

注意never 是所有类型的子类型,可赋值给任何类型;但没有类型是 never 的子类型(除了 never 本身)。


// 场景1:函数抛出异常
function throwError(message: string): never {
  throw new Error(message);  // 抛出异常后,函数永远不会正常返回
}

// 场景2:无限循环
function infiniteLoop(): never {
  while (true) {
    // 无限循环,永远不会返回
  }
}

// never 可赋值给任何类型
const num: number = throwError('出错了');  // 编译通过(虽然运行时会报错)

// 错误:其他类型不能赋值给 never
// const neverVar: never = 123;  // 类型“number”不能赋值给类型“never”

2.11 null 和 undefined

null表示空值(变量存在但无值),undefined 表示未定义(变量未赋值)。

在严格模式("strict": true)下,nullundefined 不能赋值给其他类型变量;非严格模式下可以。


const nullVar: null = null;
const undefinedVar: undefined = undefined;

// 严格模式下错误
// const str: string = null;  // 类型“null”不能赋值给类型“string”
// const num: number = undefined;  // 类型“undefined”不能赋值给类型“number”

// 非严格模式下正确
// const str: string = null;
// const num: number = undefined;

2.12 类型断言

当开发者明确知道变量的类型,而 TypeScript 无法推断时,可通过类型断言强制指定变量类型。

两种语法:

  1. 变量 as 类型:推荐(与 JSX 兼容)

  2. <类型>变量:不推荐(与 JSX 冲突)


// 示例1:unknown 类型断言为 string
let unknownVar: unknown = 'hello';
const str: string = unknownVar as string;
str.toUpperCase();  // 正确

// 示例2:DOM 元素获取(TypeScript 无法确定获取到的元素类型)
const box = document.getElementById('box') as HTMLDivElement;
box.style.width = '100px';  // 正确(此时 box 被断言为 HTMLDivElement)

// 示例3:数组元素断言
const arr: unknown[] = ['张三', 18, true];
const name = arr[0] as string;
const age = arr[1] as number;

// 注意:类型断言是“欺骗”TypeScript,若断言错误,运行时仍会报错
const num: number = '123' as number;
// num.toFixed(2);  // 运行时报错:num.toFixed is not a function

2.13 类型推断

当变量声明时未指定类型,但直接赋值,TypeScript 会自动推断变量的类型(即“类型推断”)。


// 自动推断为 number 类型
const age = 18;
// 错误:赋值类型不匹配
// age = '18';  // 类型“string”不能赋值给类型“number”

// 自动推断为 string 类型
const name = '张三';

// 自动推断为 string[] 类型
const names = ['张三', '李四'];

// 自动推断为 any 类型(未赋值时)
let anyVar;  // 推断为 any
anyVar = 123;
anyVar = 'hello';  // 正确

推荐做法:能通过类型推断确定类型的,可省略类型声明;无法推断的(如函数参数、复杂对象),明确指定类型。

第三章 变量声明与作用域

TypeScript 支持 varletconst 三种变量声明方式,推荐使用 letconst(解决 var 的作用域问题)。

3.1 var 声明(不推荐)

var 存在以下问题:

  1. 函数级作用域(无块级作用域)

  2. 变量提升(声明提前到作用域顶部)

  3. 可重复声明


// 问题1:无块级作用域
if (true) {
  var a = 10;
}
console.log(a);  // 10(if 块内声明的变量,外部可访问)

// 问题2:变量提升
console.log(b);  // undefined(声明提升,赋值未提升)
var b = 20;

// 问题3:可重复声明
var c = 30;
var c = 40;  // 不报错

3.2 let 声明(推荐)

let 解决了 var 的问题,具有:

  1. 块级作用域({} 内为作用域,外部不可访问)

  2. 无变量提升(存在“暂时性死区”,声明前不可访问)

  3. 不可重复声明


// 特性1:块级作用域
if (true) {
  let a = 10;
}
// console.log(a);  // 错误:a 未定义(块内声明,外部不可访问)

// 特性2:暂时性死区
// console.log(b);  // 错误:在初始化之前不能访问“b”
let b = 20;

// 特性3:不可重复声明
let c = 30;
// let c = 40;  // 错误:标识符“c”已声明

3.3 const 声明(推荐)

const 用于声明常量,特性与 let 一致(块级作用域、无变量提升、不可重复声明),额外限制:

  1. 声明时必须赋值

  2. 赋值后不可修改(但对象/数组的属性/元素可修改,因为 const 约束的是变量的引用,而非值本身)


// 特性1:声明时必须赋值
const a = 10;
// const b;  // 错误:const 声明必须初始化

// 特性2:基本类型不可修改
// a = 20;  // 错误:无法分配到 "a" ,因为它是常数

// 特性3:对象/数组的引用不可修改,但内部属性/元素可修改
const user = { name: '张三', age: 18 };
user.age = 19;  // 正确(修改对象属性)
console.log(user);  // { name: '张三', age: 19 }

const arr = [1, 2, 3];
arr.push(4);  // 正确(修改数组元素)
console.log(arr);  // [1, 2, 3, 4]

// 错误:修改对象/数组的引用
// user = { name: '李四' };  // 无法分配到 "user" ,因为它是常数
// arr = [4, 5, 6];  // 无法分配到 "arr" ,因为它是常数

3.4 作用域分类

  1. 全局作用域:变量在整个程序中可访问(如全局声明的 varletconst

  2. 函数作用域:变量在函数内可访问(函数参数、函数内声明的变量)

  3. 块级作用域:变量在 {} 内可访问(letconst 声明的变量)


// 全局作用域
const globalVar = '全局变量';

function fn() {
  // 函数作用域
  const funcVar = '函数变量';
  console.log(globalVar);  // 可访问全局变量

  if (true) {
    // 块级作用域
    const blockVar = '块级变量';
    console.log(funcVar);  // 可访问函数变量
    console.log(globalVar);  // 可访问全局变量
  }

  // console.log(blockVar);  // 错误:blockVar 未定义(块级作用域外部不可访问)
}

fn();
// console.log(funcVar);  // 错误:funcVar 未定义(函数作用域外部不可访问)

第四章 函数(核心重点)

TypeScript 对函数的约束主要体现在:参数类型、返回值类型、函数重载等。

4.1 函数声明与函数表达式

函数声明:function 函数名(参数1: 类型1, 参数2: 类型2): 返回值类型 { ... }

函数表达式:const 函数名 = function(参数1: 类型1, 参数2: 类型2): 返回值类型 { ... }


// 1. 函数声明
function add(a: number, b: number): number {
  return a + b;
}
const result1 = add(1, 2);  // 正确,返回 3
// const result2 = add(1, '2');  // 错误:参数2类型不匹配

// 2. 函数表达式
const subtract = function(a: number, b: number): number {
  return a - b;
};
const result3 = subtract(5, 3);  // 正确,返回 2

// 3. 箭头函数(常用)
const multiply = (a: number, b: number): number => a * b;
const result4 = multiply(4, 5);  // 正确,返回 20

// 无返回值函数(返回值类型为 void)
const log = (message: string): void => {
  console.log(message);
}
log('Hello TS');  // 正确

4.2 参数约束

4.2.1 必选参数:默认情况下,函数参数为必选,调用时必须传入对应数量的参数。


function greet(name: string): void {
  console.log(`Hello, ${name}!`);
}
greet('张三');  // 正确
// greet();  // 错误: Expected 1 arguments, but got 0.(需要 1 个参数,但只传了 0 个)

4.2.2 可选参数:在参数名后加 ?,表示该参数可选(可选参数必须放在必选参数之后)。


function greet(name: string, age?: number): void {
  if (age) {
    console.log(`Hello, ${name}! 你今年${age}岁。`);
  } else {
    console.log(`Hello, ${name}!`);
  }
}
greet('张三');  // 正确,输出:Hello, 张三!
greet('张三', 18);  // 正确,输出:Hello, 张三! 你今年18岁。

// 错误:可选参数不能放在必选参数之前
// function greet(age?: number, name: string): void { ... }

4.2.3 默认参数:给参数设置默认值,调用时若未传入该参数,使用默认值(默认参数可放在必选参数之前,但调用时需显式传入 undefined 触发默认值)。


// 默认参数放在必选参数之后(推荐)
function greet(name: string, age: number = 18): void {
  console.log(`Hello, ${name}! 你今年${age}岁。`);
}
greet('张三');  // 正确,age 使用默认值 18,输出:Hello, 张三! 你今年18岁。
greet('张三', 20);  // 正确,输出:Hello, 张三! 你今年20岁。

// 默认参数放在必选参数之前(不推荐)
function greet2(age: number = 18, name: string): void {
  console.log(`Hello, ${name}! 你今年${age}岁。`);
}
greet2(undefined, '张三');  // 正确,需显式传入 undefined 触发默认值
// greet2('张三');  // 错误:参数类型不匹配(string 不能赋值给 number)

4.2.4 剩余参数:当参数数量不确定时,使用剩余参数(...参数名: 类型[]),剩余参数必须放在参数列表最后。


// 计算多个数字的和
function sum(...nums: number[]): number {
  return nums.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2));  // 3
console.log(sum(1, 2, 3, 4));  // 10
console.log(sum());  // 0(无参数时,nums 为空数组)

// 剩余参数与必选参数结合
function fn(name: string, ...ages: number[]): void {
  console.log(name, ages);
}
fn('张三', 18, 19, 20);  // 输出:张三 [18, 19, 20]

4.3 返回值类型

函数返回值类型通过 : 返回值类型 指定,若函数无返回值,指定为 void;若函数永远不会返回,指定为 never

TypeScript 可自动推断返回值类型,但若函数体复杂(如条件分支较多),建议显式指定返回值类型。


// 自动推断返回值类型为 number
function add(a: number, b: number) {
  return a + b;
}

// 显式指定返回值类型为 string
function getUserName(id: number): string {
  if (id === 1) {
    return '张三';
  } else if (id === 2) {
    return '李四';
  }
  // 错误:缺少返回值(所有分支必须有返回值,或抛出异常)
  return '未知用户';  // 补充后正确
}

// 返回值类型为 void
function log(message: string): void {
  console.log(message);
}

// 返回值类型为 never
function throwErr(message: string): never {
  throw new Error(message);
}

4.4 函数重载

函数重载是指:同一函数名,根据不同的参数类型或参数数量,执行不同的逻辑。TypeScript 支持函数重载,需先声明重载签名,再实现函数体。

语法:


// 重载签名(只声明参数和返回值类型,不实现逻辑)
function fn(参数1: 类型1): 返回值类型1;
function fn(参数1: 类型2, 参数2: 类型2): 返回值类型2;

// 函数实现(需兼容所有重载签名)
function fn(参数1: 类型1 | 类型2, 参数2?: 类型2): 返回值类型1 | 返回值类型2 {
  // 实现逻辑
}

示例1:根据参数类型不同,返回不同结果


// 重载签名1:参数为 string,返回 string
function format(value: string): string;
// 重载签名2:参数为 number,返回 string
function format(value: number): string;

// 函数实现
function format(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase();
  } else {
    return value.toFixed(2);  // 数字保留 2 位小数
  }
}

console.log(format('hello'));  // HELLO(匹配签名1)
console.log(format(123.456));  // 123.46(匹配签名2)
// console.log(format(true));  // 错误:无匹配的重载签名

示例2:根据参数数量不同,执行不同逻辑


// 重载签名1:1个参数(name)
function greet(name: string): string;
// 重载签名2:2个参数(name, age)
function greet(name: string, age: number): string;

// 函数实现
function greet(name: string, age?: number): string {
  if (age) {
    return `Hello, ${name}! 你今年${age}岁。`;
  } else {
    return `Hello, ${name}!`;
  }
}

console.log(greet('张三'));  // Hello, 张三!(匹配签名1)
console.log(greet('张三', 18));  // Hello, 张三! 你今年18岁。(匹配签名2)
// console.log(greet('张三', '18'));  // 错误:参数2类型不匹配

4.5 箭头函数的特殊性

  1. 箭头函数没有自己的 thisthis 指向外层作用域的 this(解决传统函数this 指向问题)

  2. 不能作为构造函数(不能用new 关键字调用)

  3. 没有 arguments 对象(可使用剩余参数替代)


// 示例1:this 指向
const user = {
  name: '张三',
  age: 18,
  sayHello: function() {
    // 传统函数 this 指向 user 对象
    console.log(`Hello, ${this.name}!`);
    
    // 箭头函数 this 指向外层作用域(此处为全局作用域,非严格模式下 this 为 window)
    const fn = () => {
      console.log(`Hello, ${this.name}!`);  // 若在严格模式下,this 为 undefined
    };
    fn();
  }
};
user.sayHello();  // 输出:Hello, 张三!  和  Hello, 张三!(非严格模式)

// 示例2:不能用 new 调用
const ArrowFn = () => {};
// new ArrowFn();  // 错误:ArrowFn 不是构造函数

// 示例3:无 arguments 对象,用剩余参数替代
const sum = (...nums: number[]) => {
  // console.log(arguments);  // 错误:arguments 未定义
  return nums.reduce((total, num) => total + num, 0);
};
console.log(sum(1, 2, 3));  // 6

第五章 接口(核心重点)

接口(Interface)是 TypeScript 用于定义对象类型、函数类型、类类型的“契约”,仅用于类型检查,编译后不会生成任何 JavaScript 代码。

核心作用:约束对象的结构(属性名、属性类型、方法),确保代码符合约定。

5.1 基本对象类型接口

语法:interface 接口名 { 属性1: 类型1; 属性2: 类型2; 方法?: () => 返回值类型; }


// 定义接口(约束用户对象的结构)
interface User {
  id: number;          // 必选属性
  name: string;        // 必选属性
  age?: number;        // 可选属性(? 表示)
  readonly gender: string;  // 只读属性(readonly 表示,赋值后不可修改)
  sayHello(): void;    // 必选方法
}

// 实现接口(对象必须符合接口约束)
const user1: User = {
  id: 1,
  name: '张三',
  gender: '男',
  sayHello() {
    console.log(`Hello, ${this.name}!`);
  }
};

// 正确使用
user1.age = 18;  // 可选属性可赋值
user1.sayHello();  // 输出:Hello, 张三!

// 错误1:缺少必选属性
// const user2: User = {
//   id: 2,
//   gender: '女',
//   sayHello() {}
// };  // 错误:缺少属性“name”

// 错误2:修改只读属性
// user1.gender = '女';  // 错误:无法分配到 "gender" ,因为它是只读属性

// 错误3:多余属性
// const user3: User = {
//   id: 3,
//   name: '李四',
//   gender: '男',
//   address: '北京',  // 错误:对象文字可以只指定已知属性,并且“address”不在类型“User”中
//   sayHello() {}
// };

5.2 任意属性

当对象需要包含任意数量的额外属性时,可定义任意属性(需注意:任意属性的类型必须包含其他属性的类型)。


// 任意属性:属性名是 string 类型,属性值是 any 类型
interface User {
  id: number;
  name: string;
  [propName: string]: any;  
}

const user: User = {
  id: 1,
  name: '张三',
  age: 18,          // 额外属性(符合任意属性约束)
  address: '北京',  // 额外属性(符合任意属性约束)
  gender: '男'      // 额外属性(符合任意属性约束)
};

// 注意:任意属性的类型必须包含其他属性的类型
// 错误示例:任意属性类型为 string,无法包含 id 的 number 类型
// interface User2 {
//   id: number;
//   name: string;
//   [propName: string]: string;  // 错误:属性“id”的类型“number”不能赋给字符串索引类型“string”
// }

5.3 接口继承

接口支持继承,通过 extends 关键字实现,可继承一个或多个接口的属性和方法,实现接口的复用和扩展。

核心优势:避免重复定义相同的属性和方法,提升代码复用性;同时可基于父接口扩展新的约束。

5.3.1 单继承


// 父接口:基础用户信息
interface BaseUser {
  id: number;
  name: string;
  sayHello(): void;
}

// 子接口:继承 BaseUser,扩展额外属性
interface Student extends BaseUser {
  studentId: number;  // 学生特有属性
  study(): void;      // 学生特有方法
}

// 实现子接口:必须包含父接口和子接口的所有必选属性和方法
const student: Student = {
  id: 1,
  name: '张三',
  studentId: 2024001,
  sayHello() {
    console.log(`Hello, 我是${this.name}!`);
  },
  study() {
    console.log(`${this.name}正在学习TypeScript!`);
  }
};

student.sayHello();  // 输出:Hello, 我是张三!
student.study();     // 输出:张三正在学习TypeScript!

5.3.2 多继承

接口可同时继承多个接口,用逗号分隔父接口,子接口会包含所有父接口的属性和方法。


// 父接口1:基础信息
interface Person {
  name: string;
  age: number;
}

// 父接口2:工作信息
interface Worker {
  job: string;
  work(): void;
}

// 子接口:继承 Person 和 Worker 两个接口
interface Employee extends Person, Worker {
  employeeId: number;  // 员工特有属性
}

// 实现子接口:需包含三个接口的所有必选属性和方法
const employee: Employee = {
  name: '李四',
  age: 25,
  job: '前端开发工程师',
  employeeId: 1001,
  work() {
    console.log(`${this.name}正在开发前端页面!`);
  }
};

console.log(employee.name, employee.job);  // 输出:李四 前端开发工程师
employee.work();  // 输出:李四正在开发前端页面!

5.4 函数类型接口

接口不仅可约束对象,还可约束函数的参数类型和返回值类型,这种接口称为“函数类型接口”。

语法:interface 接口名 { (参数1: 类型1, 参数2: 类型2): 返回值类型; }


// 定义函数类型接口:约束函数的参数和返回值
interface AddFunc {
  (a: number, b: number): number;
}

// 实现函数类型接口:函数的参数和返回值必须符合接口约束
const add: AddFunc = (x, y) => x + y;  // 可省略参数类型(TypeScript 自动推断)

console.log(add(10, 20));  // 输出:30
// 错误:参数类型不匹配
// console.log(add(10, '20'));  // 类型“string”不能赋值给类型“number”

// 示例2:约束带可选参数的函数
interface GreetFunc {
  (name: string, age?: number): string;
}

const greet: GreetFunc = (name, age) => {
  if (age) {
    return `Hello, ${name}! 你今年${age}岁。`;
  } else {
    return `Hello, ${name}!`;
  }
};

console.log(greet('张三'));  // 输出:Hello, 张三!
console.log(greet('张三', 18));  // 输出:Hello, 张三! 你今年18岁。

5.5 类类型接口

类类型接口用于约束类的结构,要求类必须实现接口中定义的属性和方法(类似“抽象类”的部分功能)。通过 implements 关键字让类实现接口。



// 定义类类型接口
interface Animal {
  name: string;
  eat(): void;  // 类必须实现的方法
}

// 类实现接口:必须包含接口的所有必选属性和方法
class Dog implements Animal {
  name: string;  // 实现接口的属性
  
  // 构造函数:初始化 name
  constructor(name: string) {
    this.name = name;
  }
  
  // 实现接口的 eat 方法
  eat(): void {
    console.log(`${this.name}正在吃骨头!`);
  }
}

// 实例化 Dog 类
const dog = new Dog('旺财');
dog.eat();  // 输出:旺财正在吃骨头!

// 示例2:类实现接口时,可添加额外属性和方法
class Cat implements Animal {
  name: string;
  age: number;  // 额外属性(接口中未定义,类自身扩展)
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  
  // 实现接口的 eat 方法
  eat(): void {
    console.log(`${this.name}(${this.age}岁)正在吃小鱼干!`);
  }
  
  // 额外方法(接口中未定义,类自身扩展)
  sleep(): void {
    console.log(`${this.name}正在睡觉!`);
  }
}

// 实例化 Cat 类
const cat = new Cat('咪宝', 2);
cat.eat();  // 输出:咪宝(2岁)正在吃小鱼干!
cat.sleep();  // 输出:咪宝正在睡觉!

// 错误:类未实现接口的必选方法
// class Pig implements Animal {
//   name: string;
//   constructor(name: string) {
//     this.name = name;
//   }
//   // 缺少 eat 方法,编译报错
// }

// 注意:接口只能约束类的“实例属性”和“实例方法”,不能约束静态属性和静态方法
interface AnimalStatic {
  staticName: string;
  staticMethod(): void;
}
// 错误:类无法通过 implements 约束静态成员
// class Bird implements AnimalStatic {
//   static staticName: string = '小鸟';
//   static staticMethod(): void {
//     console.log('静态方法');
//   }
// }
// 若需约束静态成员,可通过“类的类型”来实现(后续讲解)

5.5.1 接口与抽象类的区别

类类型接口与抽象类都能约束类的结构,但核心差异在于:

  1. 抽象类:包含具体实现(可定义有方法体的方法),可被继承,编译后会生成 JavaScript 代码;

  2. 接口:仅定义契约(无方法体),不能被继承,仅用于类型检查,编译后不会生成任何代码。



// 抽象类示例
abstract class AbstractAnimal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  
  // 抽象方法(无方法体,子类必须实现)
  abstract eat(): void;
  
  // 具体方法(有方法体,子类可直接使用或重写)
  sleep(): void {
    console.log(`${this.name}正在睡觉!`);
  }
}

// 子类继承抽象类
class Rabbit extends AbstractAnimal {
  eat(): void {
    console.log(`${this.name}正在吃胡萝卜!`);
  }
}

const rabbit = new Rabbit('小白');
rabbit.eat();  // 输出:小白正在吃胡萝卜!
rabbit.sleep();  // 输出:小白正在睡觉!(继承抽象类的具体方法)

5.5.2 类实现多个接口

类可通过 implements 同时实现多个接口,用逗号分隔,需满足所有接口的约束。



// 接口1:约束 eat 方法
interface Eatable {
  eat(): void;
}

// 接口2:约束 run 方法
interface Runnable {
  run(): void;
}

// 类同时实现两个接口
class Tiger implements Eatable, Runnable {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  
  // 实现 Eatable 接口的 eat 方法
  eat(): void {
    console.log(`${this.name}正在吃肉!`);
  }
  
  // 实现 Runnable 接口的 run 方法
  run(): void {
    console.log(`${this.name}正在快速奔跑!`);
  }
}

const tiger = new Tiger('虎大王');
tiger.eat();  // 输出:虎大王正在吃肉!
tiger.run();  // 输出:虎大王正在快速奔跑!

第六章 类(核心重点)

TypeScript 完全支持 ES6 及以上的类语法,同时增加了访问修饰符、属性初始化、抽象类、泛型类等增强特性,让类的设计更严谨、更灵活。

6.1 类的基本结构

类(Class)是面向对象编程的核心,用于封装属性和方法。基本结构包括:构造函数、实例属性、实例方法、静态属性、静态方法。




class Person {
  // 1. 实例属性(默认可访问)
  name: string;
  age: number;
  
  // 2. 静态属性(属于类,不属于实例,通过类名访问)
  static species: string = '人类';
  
  // 3. 构造函数(初始化实例属性,new 时自动调用)
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  
  // 4. 实例方法(属于实例,通过实例访问)
  sayHello(): void {
    console.log(`Hello, 我是${this.name},今年${this.age}岁。`);
  }
  
  // 5. 静态方法(属于类,不属于实例,通过类名访问)
  static introduce(): void {
    console.log(`我们是${Person.species},具有思考和创造的能力。`);
  }
}

// 1. 实例化类(创建对象)
const person1 = new Person('张三', 18);
const person2 = new Person('李四', 20);

// 2. 访问实例属性和调用实例方法
console.log(person1.name);  // 输出:张三
console.log(person2.age);  // 输出:20
person1.sayHello();  // 输出:Hello, 我是张三,今年18岁。

// 3. 访问静态属性和调用静态方法
console.log(Person.species);  // 输出:人类
Person.introduce();  // 输出:我们是人类,具有思考和创造的能力。

// 错误:静态成员不能通过实例访问
// console.log(person1.species);  // 错误:属性“species”在类型“Person”的实例上不存在,但存在于类型“typeof Person”的静态成员上
// person1.introduce();  // 错误:属性“introduce”在类型“Person”上不存在,但存在于类型“typeof Person”的静态成员上

6.2 访问修饰符

TypeScript 提供了三种访问修饰符(Access Modifiers),用于控制类成员(属性、方法)的访问权限,增强代码的封装性和安全性。分别是:

  1. public:公开的(默认值),类内部、类外部、子类均可以访问;

  2. private:私有的,仅类内部可以访问,类外部和子类均无法访问;

  3. protected:受保护的,类内部和子类可以访问,类外部无法访问。

6.2.1 public 修饰符

默认所有类成员的修饰符都是 public,可省略不写。无论在类内部、外部还是子类中,都能自由访问该成员。



class Person {
  // 显式指定 public 修饰符(可省略)
  public name: string;
  public age: number;
  
  public constructor(name: string, age: number) {
    this.name = name;  // 类内部访问 public 成员
    this.age = age;
  }
  
  public sayHello(): void {
    console.log(`Hello, 我是${this.name}(public 修饰)`);  // 类内部访问
  }
}

// 类外部访问 public 成员
const person = new Person('张三', 18);
console.log(person.name);  // 输出:张三
console.log(person.age);   // 输出:18
person.sayHello();         // 输出:Hello, 我是张三(public 修饰)

// 子类访问父类的 public 成员
class Student extends Person {
  study(): void {
    console.log(`${this.name}正在学习`);  // 子类访问父类的 public 属性 name
  }
}
const student = new Student('李四', 20);
student.study();  // 输出:李四正在学习

6.2.2 private 修饰符

private 修饰的成员仅能在类内部访问,类外部直接访问会报错;子类也无法访问父类的 private 成员。



class Person {
  public name: string;
  // 私有属性:仅类内部可访问
  private idCard: string;
  
  constructor(name: string, idCard: string) {
    this.name = name;
    this.idCard = idCard;  // 类内部访问 private 成员(正确)
  }
  
  // 私有方法:仅类内部可访问
  private checkIdCard(): void {
    console.log(`验证身份证:${this.idCard}`);
  }
  
  public introduce(): void {
    this.checkIdCard();  // 类内部调用 private 方法(正确)
    console.log(`我是${this.name}`);
  }
}

const person = new Person('张三', '110101199001011234');
console.log(person.name);  // 输出:张三(public 可访问)
person.introduce();        // 输出:验证身份证:110101199001011234  我是张三

// 错误:类外部访问 private 成员
// console.log(person.idCard);  // 错误:属性“idCard”为私有属性,只能在类“Person”中访问
// person.checkIdCard();        // 错误:属性“checkIdCard”为私有属性,只能在类“Person”中访问

// 错误:子类访问父类的 private 成员
class Student extends Person {
  study(): void {
    // console.log(this.idCard);  // 错误:属性“idCard”为私有属性,只能在类“Person”中访问
  }
}

6.2.3 protected 修饰符

protected 修饰的成员可在类内部和子类中访问,但类外部无法访问。适合需要在子类中复用、但不希望外部暴露的成员。



class Person {
  public name: string;
  // 受保护属性:类内部和子类可访问
  protected familyName: string;
  
  constructor(name: string, familyName: string) {
    this.name = name;
    this.familyName = familyName;  // 类内部访问(正确)
  }
  
  // 受保护方法:类内部和子类可访问
  protected getFullName(): string {
    return `${this.familyName}${this.name}`;
  }
}

// 类外部访问 protected 成员(错误)
const person = new Person('三', '张');
// console.log(person.familyName);  // 错误:属性“familyName”受保护,只能在类“Person”及其子类中访问
// console.log(person.getFullName());  // 错误:属性“getFullName”受保护,只能在类“Person”及其子类中访问

// 子类访问父类的 protected 成员(正确)
class Student extends Person {
  study(): void {
    const fullName = this.getFullName();  // 子类访问父类 protected 方法
    console.log(`${fullName}正在学习`);
  }
}
const student = new Student('四', '李');
student.study();  // 输出:李四正在学习

6.2.4 构造函数中的访问修饰符(简化写法)

在构造函数参数前添加访问修饰符,可以直接省略类内的属性声明和赋值步骤,TypeScript 会自动将参数声明为对应修饰符的属性,并完成赋值。这是日常开发中常用的简化写法。



// 简化前:需手动声明属性并赋值
class Person1 {
  public name: string;
  private age: number;
  protected gender: string;
  
  constructor(name: string, age: number, gender: string) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
}

// 简化后:构造函数参数前加修饰符,自动声明属性并赋值
class Person2 {
  constructor(
    public name: string,
    private age: number,
    protected gender: string
  ) {}  // 无需额外写赋值逻辑
}

// 使用效果完全一致
const p1 = new Person1('张三', 18, '男');
const p2 = new Person2('李四', 20, '女');
console.log(p1.name);  // 输出:张三
console.log(p2.name);  // 输出:李四
// console.log(p2.age);  // 错误:private 成员无法访问
// console.log(p2.gender);  // 错误:protected 成员无法访问

6.3 只读属性(readonly)

readonly 用于声明只读属性,属性只能在声明时或构造函数中赋值,赋值后无法修改。常与访问修饰符结合使用,进一步增强数据安全性。



class Person {
  // 单独使用 readonly(默认 public)
  readonly id: number;
  // 与访问修饰符结合使用(推荐,明确权限)
  public readonly name: string;
  private readonly birthDate: string;
  
  constructor(id: number, name: string, birthDate: string) {
    this.id = id;          // 构造函数中赋值(正确)
    this.name = name;
    this.birthDate = birthDate;
  }
}

const person = new Person(1, '张三', '1990-01-01');
console.log(person.id);    // 输出:1
console.log(person.name);  // 输出:张三

// 错误:修改 readonly 属性
// person.id = 2;          // 错误:无法分配到 "id" ,因为它是只读属性
// person.name = '李四';  // 错误:无法分配到 "name" ,因为它是只读属性
// console.log(person.birthDate);  // 错误:private 成员无法访问

// 注意:readonly 仅约束属性的“赋值行为”,若属性是对象/数组,其内部属性/元素仍可修改
class User {
  readonly info: { name: string; age: number };
  
  constructor(info: { name: string; age: number }) {
    this.info = info;
  }
}
const user = new User({ name: '张三', age: 18 });
user.info.age = 19;  // 正确:修改对象内部属性
console.log(user.info);  // 输出:{ name: '张三', age: 19 }
// user.info = { name: '李四', age: 20 };  // 错误:修改 readonly 属性本身(引用)

6.4 类的继承

继承是面向对象编程的核心特性之一,通过 extends 关键字让子类继承父类的属性和方法,实现代码复用;同时子类可扩展自身的属性和方法,或重写父类的方法。

语法:class 子类名 extends 父类名 { ... }

6.4.1 基本继承用法

子类继承父类后,会自动拥有父类的所有 publicprotected 成员(private 成员因权限限制无法继承)。



// 父类:Person
class Person {
  public name: string;
  protected age: number;
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  
  public sayHello(): void {
    console.log(`Hello, 我是${this.name},今年${this.age}岁。`);
  }
}

// 子类:Student 继承 Person
class Student extends Person {
  // 子类扩展自身的属性(学号)
  public studentId: number;
  
  // 子类构造函数:必须调用 super() 继承父类属性
  constructor(name: string, age: number, studentId: number) {
    super(name, age);  // 调用父类的构造函数,初始化继承的属性(必须放在子类构造函数第一行)
    this.studentId = studentId;  // 初始化子类自身的属性
  }
  
  // 子类扩展自身的方法(学习)
  public study(): void {
    console.log(`${this.name}(学号:${this.studentId})正在学习TypeScript。`);
  }
}

// 实例化子类
const student = new Student('张三', 18, 2024001);

// 调用继承的父类方法
student.sayHello();  // 输出:Hello, 我是${this.name},今年${this.age}岁。
// 调用子类自身的方法
student.study();     // 输出:张三(学号:2024001)正在学习TypeScript。
// 访问继承的父类属性(public)
console.log(student.name);  // 输出:张三
// 无法访问父类的 protected 属性(类外部)
// console.log(student.age);  // 错误:属性“age”受保护,只能在类“Person”及其子类中访问

6.4.2 继承中的构造函数规则

  1. 若子类没有定义构造函数,会自动继承父类的构造函数,实例化时需传入父类构造函数要求的参数;

  2. 若子类定义了构造函数,super() 必须在构造函数第一行调用,用于初始化父类的属性;

  3. super() 的参数需与父类构造函数的参数匹配;

  4. 子类构造函数的参数可以比父类多,用于扩展自身属性。



// 父类
class Person {
  public name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// 子类1:无构造函数(自动继承父类构造函数)
class Teacher extends Person {
  public subject: string = '数学';  // 扩展属性(默认值)
}
// 实例化时需传入父类构造函数的参数
const teacher1 = new Teacher('李老师');
console.log(teacher1.name);    // 输出:李老师
console.log(teacher1.subject); // 输出:数学

// 子类2:有构造函数(必须调用 super())
class Teacher2 extends Person {
  public subject: string;
  constructor(name: string, subject: string) {
    super(name);  // 调用父类构造函数(必须在第一行)
    this.subject = subject;  // 初始化子类属性
  }
}
const teacher2 = new Teacher2('王老师', '英语');
console.log(teacher2.name);    // 输出:王老师
console.log(teacher2.subject); // 输出:英语

// 错误:子类构造函数未调用 super()
// class Teacher3 extends Person {
//   constructor(name: string) {
//     this.name = name;  // 错误:构造函数中没有调用 super()
//   }
// }

6.4.3 方法重写(Override)

子类可重写父类的方法(即定义与父类同名的方法),实现子类特有的逻辑。重写时需注意:

  1. 方法名必须与父类完全一致;

  2. 方法的参数类型、参数数量、返回值类型需与父类兼容(推荐完全一致);

  3. 可使用 override 关键字显式声明重写(TypeScript 4.3+ 支持),增强类型检查,避免误写方法名。



// 父类
class Person {
  public name: string;
  constructor(name: string) {
    this.name = name;
  }
  
  public sayHello(): void {
    console.log(`Hello, 我是${this.name}(父类方法)`);
  }
}

// 子类重写父类方法(基础写法)
class Student extends Person {
  constructor(name: string) {
    super(name);
  }
  
  // 重写父类的 sayHello 方法
  public sayHello(): void {
    console.log(`Hello, 我是学生${this.name}(子类重写方法)`);
  }
}
const student = new Student('张三');
student.sayHello();  // 输出:Hello, 我是学生张三(子类重写方法)

// 子类重写方法(使用 override 关键字,推荐)
class Teacher extends Person {
  constructor(name: string) {
    super(name);
  }
  
  // override 显式声明重写,若父类无该方法则报错
  public override sayHello(): void {
    console.log(`Hello, 我是老师${this.name}(子类重写方法)`);
  }
}
const teacher = new Teacher('李老师');
teacher.sayHello();  // 输出:Hello, 我是老师李老师(子类重写方法)

// 错误:重写方法与父类返回值类型不兼容
// class Doctor extends Person {
//   constructor(name: string) {
//     super(name);
//   }
//   // 父类返回值为 void,子类返回值为 string,不兼容
//   public override sayHello(): string {
//     return `Hello, 我是医生${this.name}`;
//   }
// }

6.4.4 重写时访问父类方法

子类重写父类方法后,若需在子类方法中调用父类的原方法,可通过 super.方法名() 实现。



class Person {
  public name: string;
  constructor(name: string) {
    this.name = name;
  }
  
  public sayHello(): void {
    console.log(`父类:Hello, 我是${this.name}`);
  }
}

class Student extends Person {
  constructor(name: string) {
    super(name);
  }
  
  public override sayHello(): void {
    super.sayHello();  // 调用父类的 sayHello 方法
    console.log(`子类:我是学生${this.name},正在学习`);
  }
}

const student = new Student('张三');
student.sayHello();
// 输出:
// 父类:Hello, 我是张三
// 子类:我是学生张三,正在学习

6.5 抽象类(Abstract Class)

抽象类是一种“不能被实例化”的类,仅用于被继承(作为父类的模板)。抽象类可包含抽象方法(无方法体,子类必须重写)和具体方法(有方法体,子类可直接使用或重写)。

核心作用:定义统一的模板结构,约束子类必须实现特定方法,提升代码的规范性和可维护性。

语法:



// 定义抽象类(abstract 关键字)
abstract class 抽象类名 {
  // 具体方法(有方法体)
  方法名(): 返回值类型 { ... }
  
  // 抽象方法(无方法体,abstract 关键字,子类必须重写)
  abstract 抽象方法名(): 返回值类型;
}

6.5.1 抽象类的基本使用



// 抽象类:Animal(不能实例化)
abstract class Animal {
  public name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  // 具体方法(有方法体,子类可直接使用)
  public sleep(): void {
    console.log(`${this.name}正在睡觉`);
  }
  
  // 抽象方法(无方法体,子类必须重写)
  abstract eat(): void;
}

// 错误:抽象类不能实例化
// const animal = new Animal('动物');  // 错误:无法创建抽象类的实例

// 子类1:Dog 继承抽象类,必须重写 eat 方法
class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }
  
  // 重写抽象方法 eat(必须实现)
  override eat(): void {
    console.log(`${this.name}正在吃骨头`);
  }
}
const dog = new Dog('旺财');
dog.eat();   // 输出:旺财正在吃骨头
dog.sleep(); // 输出:旺财正在睡觉

// 子类2:Cat 继承抽象类,重写 eat 方法并扩展自身方法
class Cat extends Animal {
  constructor(name: string) {
    super(name);
  }
  
  override eat(): void {
    console.log(`${this.name}正在吃小鱼干`);
  }
  
  // 扩展自身方法
  public catchMouse(): void {
    console.log(`${this.name}正在抓老鼠`);
  }
}
const cat = new Cat('咪宝');
cat.eat();         // 输出:咪宝正在吃小鱼干
cat.sleep();       // 输出:咪宝正在睡觉
cat.catchMouse();  // 输出:咪宝正在抓老鼠

// 错误:子类未重写抽象方法
// class Pig extends Animal {
//   constructor(name: string) {
//     super(name);
//   }
//   // 缺少 eat 抽象方法的实现,编译报错
// }

6.5.2 抽象类与接口的区别

抽象类和接口都能约束子类/实现类的结构,但适用场景不同,核心区别如下:

  1. 实例化:抽象类不能实例化,但可包含构造函数;接口不能实例化,也无构造函数;

  2. 方法类型:抽象类可包含具体方法(有方法体)和抽象方法(无方法体);接口仅能定义方法契约(无方法体);

  3. 继承/实现:类只能继承一个抽象类(单继承);类可实现多个接口(多实现);

  4. 成员权限:抽象类的成员可使用 publicprivateprotected 修饰;接口的成员默认是 public,不能使用其他修饰符;

  5. 编译产物:抽象类编译后会生成 JavaScript 代码;接口仅用于类型检查,编译后无产物。




// 抽象类示例(含具体方法和抽象方法)
abstract class AbstractAnimal {
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }
  abstract eat(): void;
  public sleep(): void {
    console.log(`${this.name}睡觉`);
  }
}

// 接口示例(仅方法契约)
interface IAnimal {
  name: string;
  eat(): void;
  sleep(): void;
}

// 类继承抽象类(单继承)
class Dog extends AbstractAnimal {
  override eat(): void {
    console.log(`${this.name}吃骨头`);
  }
}

// 类实现接口(多实现)
class Cat implements IAnimal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat(): void {
    console.log(`${this.name}吃小鱼干`);
  }
  sleep(): void {&#xA;    console.log(`${this.name}睡觉`);&#xA;  }&#xA;}&#xA;

6.6 泛型类(Generic Class)

泛型类是将泛型(参数化类型)应用于类的定义,使类能够适配多种数据类型,而无需为每种类型单独定义类,极大提升了类的复用性和灵活性。泛型类的核心思想是“将类型作为参数传递”,让类的属性、方法能基于传递的类型动态适配。

语法:class 类名<泛型参数1, 泛型参数2 = 默认类型> { ... },其中泛型参数通常用大写字母(如 T、U、V)表示,代表“类型占位符”。

6.6.1 泛型类的基本使用

场景:定义一个通用的“容器”类,可存储不同类型的数据(如数字、字符串、对象),并提供添加、获取数据的方法。若不使用泛型,需为每种类型定义单独的容器类(如 NumberContainer、StringContainer),代码冗余;使用泛型可统一实现。



// 定义泛型类:<T> 是泛型参数,代表待确定的类型
class Container<T> {
  // 泛型属性:类型为 T
  private data: T;
  
  // 构造函数:参数类型为 T
  constructor(initialData: T) {
    this.data = initialData;
  }
  
  // 泛型方法:设置数据,参数类型为 T
  setData(newData: T): void {
    this.data = newData;
  }
  
  // 泛型方法:获取数据,返回值类型为 T
  getData(): T {
    return this.data;
  }
}

// 1. 使用泛型类存储 number 类型
const numberContainer = new Container<number>(10);
numberContainer.setData(20);
console.log(numberContainer.getData());  // 输出:20(类型为 number)

// 2. 使用泛型类存储 string 类型
const stringContainer = new Container<string>('Hello');
stringContainer.setData('Hello TypeScript');
console.log(stringContainer.getData());  // 输出:Hello TypeScript(类型为 string)

// 3. 使用泛型类存储对象类型(自定义接口类型)
interface User {
  name: string;
  age: number;
}
const userContainer = new Container<User>({ name: '张三', age: 18 });
userContainer.setData({ name: '李四', age: 20 });
console.log(userContainer.getData());  // 输出:{ name: '李四', age: 20 }(类型为 User)

// 4. 类型推断:实例化时可省略泛型参数,TypeScript 自动根据初始值推断类型
const booleanContainer = new Container(true);  // 自动推断 T 为 boolean
booleanContainer.setData(false);
console.log(booleanContainer.getData());  // 输出:false(类型为 boolean)

// 错误:类型不匹配(泛型参数约束了数据类型)
// numberContainer.setData('20');  // 错误:类型“string”不能赋值给类型“number”
// stringContainer.setData(30);    // 错误:类型“number”不能赋值给类型“string”

6.6.2 泛型类的约束(Generic Constraint)

默认情况下,泛型参数可以是任意类型,但有时需要限制泛型参数的范围(如仅允许具有特定属性或方法的类型),此时可通过“泛型约束”实现。泛型约束通常结合接口使用,明确要求泛型参数必须满足接口的结构。

语法:class 类名<T extends 约束类型> { ... },其中 extends 表示“泛型参数 T 必须是约束类型的子类型”。

场景:定义一个泛型类,要求传入的类型必须具有 length 属性(如字符串、数组、对象),用于获取数据的长度。



// 定义约束接口:要求类型必须具有 length 属性
interface HasLength {
  length: number;
}

// 定义带约束的泛型类:T 必须继承 HasLength(即 T 必须具有 length 属性)
class LengthCalculator<T extends HasLength> {
  private data: T;
  
  constructor(data: T) {
    this.data = data;
  }
  
  // 计算数据的长度
  getLength(): number {
    return this.data.length;
  }
}

// 1. 传入 string 类型(具有 length 属性,符合约束)
const stringLengthCalc = new LengthCalculator<string>('Hello TS');
console.log(stringLengthCalc.getLength());  // 输出:8

// 2. 传入数组类型(具有 length 属性,符合约束)
const arrayLengthCalc = new LengthCalculator<number[]>([1, 2, 3, 4]);
console.log(arrayLengthCalc.getLength());  // 输出:4

// 3. 传入自定义对象类型(具有 length 属性,符合约束)
const objWithLength = { name: '测试对象', length: 5 };
const objLengthCalc = new LengthCalculator(objWithLength);
console.log(objLengthCalc.getLength());  // 输出:5

// 错误:传入不具有 length 属性的类型(不符合约束)
// const numberLengthCalc = new LengthCalculator<number>(123);
// 错误:类型“number”不满足约束“HasLength”。类型“number”上不存在属性“length”

6.6.3 泛型类的多泛型参数

泛型类支持定义多个泛型参数,用于适配更复杂的场景(如需要同时处理两种不同类型的数据)。多个泛型参数用逗号分隔,可分别指定约束和默认值。

场景:定义一个“键值对”泛型类,存储键(key)和值(value)两种不同类型的数据。



// 定义多泛型参数的类:K 表示键的类型,V 表示值的类型
class KeyValuePair<K, V> {
  private key: K;
  private value: V;
  
  constructor(key: K, value: V) {
    this.key = key;
    this.value = value;
  }
  
  // 获取键值对
  getPair(): { key: K; value: V } {
    return { key: this.key, value: this.value };
  }
  
  // 更新值
  updateValue(newValue: V): void {
    this.value = newValue;
  }
}

// 1. 键为 string 类型,值为 number 类型
const pair1 = new KeyValuePair<string, number>('age', 18);
console.log(pair1.getPair());  // 输出:{ key: 'age', value: 18 }
pair1.updateValue(20);
console.log(pair1.getPair());  // 输出:{ key: 'age', value: 20 }

// 2. 键为 number 类型,值为 string 类型
const pair2 = new KeyValuePair<number, string>(1, '张三');
console.log(pair2.getPair());  // 输出:{ key: 1, value: '张三' }

// 3. 键为 symbol 类型,值为对象类型
const key = Symbol('user');
const user = { name: '李四', gender: '男' };
const pair3 = new KeyValuePair<symbol, typeof user>(key, user);
console.log(pair3.getPair());  // 输出:{ key: Symbol(user), value: { name: '李四', gender: '男' } }

6.6.4 泛型类与接口的结合

泛型类可实现泛型接口,进一步约束类的结构。泛型接口中的泛型参数可由泛型类传递,实现更灵活的类型约束。



// 定义泛型接口:约束类必须具有 add 和 get 方法
interface ICollection<T> {
  add(item: T): void;
  get(index: number): T | undefined;
}

// 泛型类实现泛型接口:传递泛型参数 T
class List<T> implements ICollection<T> {
  private items: T[] = [];
  
  // 实现接口的 add 方法
  add(item: T): void {
    this.items.push(item);
  }
  
  // 实现接口的 get 方法
  get(index: number): T | undefined {
    return this.items[index];
  }
  
  // 扩展自身方法:获取列表长度
  getSize(): number {
    return this.items.length;
  }
}

// 使用泛型类 List 存储 string 类型
const stringList = new List<string>();
stringList.add('张三');
stringList.add('李四');
console.log(stringList.get(0));  // 输出:张三
console.log(stringList.getSize());  // 输出:2

// 使用泛型类 List 存储 number 类型
const numberList = new List<number>();
numberList.add(10);
numberList.add(20);
console.log(numberList.get(1));  // 输出:20
console.log(numberList.getSize());  // 输出:2

泛型类是 TypeScript 中实现代码复用的核心工具之一,尤其适合开发通用组件(如容器、列表、表格等),在 React、Vue 等框架的类型定义中被广泛应用。掌握泛型类的核心是理解“类型参数化”的思想,通过泛型参数将类型的确定延迟到实例化阶段,从而适配多种场景。

第七章 泛型(深入详解)

前文在泛型类中初步接触了泛型的概念,本章将系统讲解泛型的完整用法。泛型(Generic)的核心思想是“类型参数化”,即把类型作为参数传递给函数、类、接口,使它们能够适配多种数据类型,同时保持类型安全。泛型解决了“重复定义相似类型代码”的问题,是 TypeScript 实现代码复用和类型抽象的重要手段。

7.1 泛型函数

泛型函数是泛型最常用的场景,通过定义泛型参数,使函数能够接收不同类型的参数并返回对应类型的结果,避免为每种类型单独定义函数。

语法:function 函数名<泛型参数>(参数: 泛型参数): 泛型参数 { ... }

7.1.1 基本使用


// 泛型函数:返回与参数类型相同的值
function identity<T>(arg: T): T {
  return arg;
}

// 1. 显式指定泛型参数
const num = identity<number>(10); // T = number,返回值类型为 number
const str = identity<string>('hello'); // T = string,返回值类型为 string
const obj = identity<{ name: string }>({ name: '张三' }); // T = 自定义对象类型

// 2. 类型推断(推荐):可省略泛型参数,TypeScript 自动根据参数类型推断 T
const bool = identity(true); // 自动推断 T = boolean
const arr = identity([1, 2, 3]); // 自动推断 T = number[]

console.log(num); // 10
console.log(str); // hello
console.log(obj); // { name: '张三' }
console.log(bool); // true
console.log(arr); // [1, 2, 3]

7.1.2 泛型函数与数组结合

当泛型参数为数组类型时,可通过 T[]Array<T> 约束参数,同时可使用数组的内置方法。


// 泛型函数:获取数组的第一个元素
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

// 类型推断:T = number
const firstNum = getFirstElement([1, 2, 3]); 
console.log(firstNum); // 1

// 类型推断:T = string
const firstStr = getFirstElement(['张三', '李四']);
console.log(firstStr); // 张三

// 类型推断:T = { name: string; age: number }
const users = [{ name: '张三', age: 18 }, { name: '李四', age: 20 }];
const firstUser = getFirstElement(users);
console.log(firstUser?.name); // 张三

7.1.3 多泛型参数函数

泛型函数支持多个泛型参数,用于处理多种不同类型的参数,多个参数用逗号分隔。


// 泛型函数:交换两个不同类型的值(返回元组)
function swap<T, U>(a: T, b: U): [U, T] {
  return [b, a];
}

// 类型推断:T = number,U = string
const [swapped1, swapped2] = swap(10, 'hello');
console.log(swapped1); // hello(类型 U = string)
console.log(swapped2); // 10(类型 T = number)

// 类型推断:T = boolean,U = { name: string }
const [swapped3, swapped4] = swap(true, { name: '张三' });
console.log(swapped3); // { name: '张三' }(类型 U)
console.log(swapped4); // true(类型 T)

7.2 泛型约束进阶

默认情况下,泛型参数可以是任意类型,但有时需要限制泛型参数的范围(如仅允许具有特定属性或方法的类型),此时可通过 extends 实现泛型约束,结合接口或类型别名明确约束条件。

7.2.1 约束泛型具有特定属性


// 定义约束接口:要求类型必须具有 length 属性
interface HasLength {
  length: number;
}

// 泛型函数:约束 T 必须继承 HasLength(即 T 必须有 length 属性)
function getLength<T extends HasLength>(arg: T): number {
  return arg.length;
}

// 正确:string 具有 length 属性
console.log(getLength('hello')); // 5

// 正确:数组具有 length 属性
console.log(getLength([1, 2, 3])); // 3

// 正确:自定义对象具有 length 属性
console.log(getLength({ name: '测试', length: 10 })); // 10

// 错误:number 没有 length 属性,不符合约束
// console.log(getLength(123)); // 类型“number”不满足约束“HasLength”

7.2.2 约束泛型为对象的属性键

通过 keyof 操作符可获取对象的所有属性键组成的联合类型,结合泛型约束可实现“安全访问对象属性”的功能。


// 泛型函数:安全获取对象的属性值
// T 约束为对象类型,K 约束为 T 的属性键(keyof T)
function getProperty<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = {
  name: '张三',
  age: 18,
  gender: '男'
};

// 正确:key = 'name' 是 user 的属性键
console.log(getProperty(user, 'name')); // 张三(类型 T[K] = string)

// 正确:key = 'age' 是 user 的属性键
console.log(getProperty(user, 'age')); // 18(类型 T[K] = number)

// 错误:key = 'address' 不是 user 的属性键,不符合约束
// console.log(getProperty(user, 'address')); // 类型“string”的参数不能赋给类型“keyof { name: string; age: number; gender: string; }”的参数

7.3 泛型工具类型

TypeScript 内置了多个泛型工具类型,用于简化常见的类型转换场景,无需手动编写复杂的类型逻辑。常用的工具类型包括 PartialRequiredReadonlyRecordPickOmit 等。

7.3.1 Partial:将 T 的所有属性变为可选


interface User {
  id: number;
  name: string;
  age: number;
}

// Partial<User>:将 User 的所有属性变为可选(id?: number; name?: string; age?: number)
function updateUser(user: User, updates: Partial<User>): User {
  return { ...user, ...updates };
}

const user = { id: 1, name: '张三', age: 18 };
// 仅更新部分属性(符合 Partial<User> 约束)
const updatedUser = updateUser(user, { age: 19, name: '张三三' });
console.log(updatedUser); // { id: 1, name: '张三三', age: 19 }

7.3.2 Required:将 T 的所有属性变为必选


interface PartialUser {
  id?: number;
  name?: string;
  age?: number;
}

// Required<PartialUser>:将 PartialUser 的所有属性变为必选(id: number; name: string; age: number)
const fullUser: Required<PartialUser> = {
  id: 1,
  name: '张三',
  age: 18
};

// 错误:缺少必选属性
// const incompleteUser: Required<PartialUser> = { id: 1 }; // 缺少属性“name”和“age”

7.3.3 Readonly:将 T 的所有属性变为只读


interface User {
  id: number;
  name: string;
}

// Readonly<User>:将 User 的所有属性变为只读(readonly id: number; readonly name: string)
const readonlyUser: Readonly<User> = { id: 1, name: '张三' };

// 错误:无法修改只读属性
// readonlyUser.name = '李四'; // 无法分配到 "name" ,因为它是只读属性

7.3.4 Record<K, T>:定义键为 K 类型、值为 T 类型的对象


// Record<string, User>:键为 string 类型,值为 User 类型的对象
const userMap: Record<string, User> = {
  'user1': { id: 1, name: '张三', age: 18 },
  'user2': { id: 2, name: '李四', age: 20 }
};

// 正确:键为 string,值为 User
userMap['user3'] = { id: 3, name: '王五', age: 22 };

// 错误:值类型不匹配
// userMap['user4'] = { id: 4 }; // 缺少属性“name”和“age”,类型不匹配 User

7.3.5 Pick<T, K>:从 T 中挑选出属性 K 组成新类型


interface User {
  id: number;
  name: string;
  age: number;
  gender: string;
}

// Pick<User, 'name' | 'age'>:从 User 中挑选 name 和 age 属性组成新类型
type UserInfo = Pick<User, 'name' | 'age'>;

const userInfo: UserInfo = {
  name: '张三',
  age: 18
};

// 错误:包含了未挑选的属性
// const invalidUserInfo: UserInfo = { name: '张三', age: 18, id: 1 }; // 属性“id”不在类型“UserInfo”中

7.3.6 Omit<T, K>:从 T 中排除属性 K 组成新类型


interface User {
  id: number;
  name: string;
  age: number;
  gender: string;
}

// Omit<User, 'id' | 'gender'>:从 User 中排除 id 和 gender 属性组成新类型
type UserBasic = Omit<User, 'id' | 'gender'>;

const userBasic: UserBasic = {
  name: '张三',
  age: 18
};

// 错误:包含了被排除的属性
// const invalidUserBasic: UserBasic = { name: '张三', age: 18, id: 1 }; // 属性“id”不在类型“UserBasic”中

第八章 高级类型

除了基础类型和泛型,TypeScript 还提供了多种高级类型,用于处理复杂的类型场景,如联合类型、交叉类型、类型别名、条件类型、映射类型等。掌握这些高级类型可以大幅提升 TypeScript 的类型设计能力,适应中大型项目的复杂需求。

8.1 联合类型(Union Type)

联合类型表示一个值可以是多种类型中的一种,使用 | 分隔多个类型。联合类型的核心是“或”的关系,即值只需匹配其中一种类型即可。

8.1.1 基本使用


// 联合类型:value 可以是 number 或 string 类型
type NumberOrString = number | string;

function formatValue(value: NumberOrString): string {
  if (typeof value === 'number') {
    // 类型守卫:此时 value 被推断为 number 类型
    return value.toFixed(2);
  } else {
    // 此时 value 被推断为 string 类型
    return value.toUpperCase();
  }
}

console.log(formatValue(123.456)); // 123.46(number 类型处理)
console.log(formatValue('hello')); // HELLO(string 类型处理)

// 错误:类型不匹配(既不是 number 也不是 string)
// formatValue(true); // 类型“boolean”的参数不能赋给类型“NumberOrString”的参数

8.1.2 联合类型与对象

联合类型可用于多个对象类型的组合,此时值需匹配其中一个对象的结构。


interface Student {
  type: 'student';
  id: number;
  studentId: number;
  name: string;
}

interface Teacher {
  type: 'teacher';
  id: number;
  teacherId: number;
  name: string;
}

// 联合类型:Person 可以是 Student 或 Teacher
type Person = Student | Teacher;

// 类型守卫:通过 type 属性区分具体类型( discriminated union 区分联合类型)
function printPersonInfo(person: Person): void {
  console.log(`姓名:${person.name},ID:${person.id}`);
  if (person.type === 'student') {
    console.log(`学生号:${person.studentId}`);
  } else {
    console.log(`教师号:${person.teacherId}`);
  }
}

const student: Student = {
  type: 'student',
  id: 1,
  studentId: 2024001,
  name: '张三'
};

const teacher: Teacher = {
  type: 'teacher',
  id: 2,
  teacherId: 1001,
  name: '李老师'
};

printPersonInfo(student);
// 输出:
// 姓名:张三,ID:1
// 学生号:2024001

printPersonInfo(teacher);
// 输出:
// 姓名:李老师,ID:2
// 教师号:1001

8.2 交叉类型(Intersection Type)

交叉类型表示一个值需要同时具备多种类型的所有属性和方法,使用 & 分隔多个类型。交叉类型的核心是“且”的关系,即值必须匹配所有类型的约束。

8.2.1 基本使用


interface Name {
  name: string;
}

interface Age {
  age: number;
}

// 交叉类型:Person 同时具备 Name 和 Age 的属性(name: string; age: number)
type Person = Name & Age;

const person: Person = {
  name: '张三',
  age: 18
};

// 错误:缺少 Age 的 age 属性
// const invalidPerson: Person = { name: '李四' }; // 缺少属性“age”

8.2.2 交叉类型与接口继承

交叉类型可实现类似接口多继承的效果,但更灵活(可交叉任意类型,包括基础类型、类型别名等)。


interface User {
  id: number;
  name: string;
}

interface Employee {
  employeeId: number;
  department: string;
}

// 交叉类型:Staff 同时具备 User 和 Employee 的属性
type Staff = User & Employee;

const staff: Staff = {
  id: 1,
  name: '张三',
  employeeId: 1001,
  department: '研发部'
};

// 交叉类型与基础类型(少见,需注意兼容性)
type NumberAndString = number & string;
// 不存在同时是 number 和 string 的值,因此 NumberAndString 是 never 类型
// const ns: NumberAndString = 123; // 类型“number”不能赋给类型“never”

8.3 类型别名(Type Alias)

类型别名用于给已有类型起一个新名字,使用 type 关键字定义。类型别名可简化复杂类型的书写,提高代码可读性,支持基础类型、联合类型、交叉类型、对象类型等多种类型。

8.3.1 基本使用


// 给基础类型起别名
type MyString = string;
type MyNumber = number;

const str: MyString = 'hello';
const num: MyNumber = 123;

// 给联合类型起别名
type NumberOrString = number | string;
type Status = 'success' | 'fail' | 'pending'; // 字符串字面量联合类型

// 给交叉类型起别名
type User = { id: number; name: string };
type Employee = { employeeId: number };
type Staff = User & Employee;

// 给对象类型起别名
type Point = {
  x: number;
  y: number;
  z?: number; // 可选属性
};

const point: Point = { x: 10, y: 20 };
const point3d: Point = { x: 10, y: 20, z: 30 };

8.3.2 类型别名与接口的区别

类型别名和接口都可用于定义对象类型,且功能相似,但存在以下核心区别:

  1. 扩展方式:接口通过 extends 继承扩展;类型别名通过交叉类型 & 扩展;

  2. 重复定义:接口支持重复定义,会自动合并属性;类型别名不支持重复定义(会报错);

  3. 适用场景:接口适合定义对象/类的结构(强调“契约”);类型别名适合定义联合类型、交叉类型、字面量类型等复杂类型(强调“别名”)。


// 1. 接口扩展(extends)
interface User {
  id: number;
  name: string;
}

interface User extends { age: number } {} // 扩展 User 接口,添加 age 属性
const user: User = { id: 1, name: '张三', age: 18 }; // 正确

// 2. 类型别名扩展(交叉类型)
type UserAlias = { id: number; name: string };
type UserAliasWithAge = UserAlias & { age: number }; // 交叉扩展
const userAlias: UserAliasWithAge = { id: 1, name: '张三', age: 18 }; // 正确

// 3. 接口重复定义(自动合并)
interface Point { x: number; }
interface Point { y: number; } // 重复定义,自动合并
const point: Point = { x: 10, y: 20 }; // 正确

// 4. 类型别名重复定义(报错)
type PointAlias = { x: number };
// type PointAlias = { y: number }; // 错误:标识符“PointAlias”重复

8.4 条件类型(Conditional Type)

条件类型是一种“类型层面的三元表达式”,根据一个类型是否满足某个条件,返回不同的类型。语法:T extends U ? X : Y(若 T 是 U 的子类型,则返回 X 类型,否则返回 Y 类型)。

8.4.1 基本使用


// 条件类型:若 T 是 string 类型,则返回 string,否则返回 number
type StringOrNumber<T> = T extends string ? string : number;

// 类型推断:T = string → 返回 string
type A = StringOrNumber<string>; // A = string

// 类型推断:T = number → 返回 number
type B = StringOrNumber<number>; // B = number

// 类型推断:T = boolean → 返回 number
type C = StringOrNumber<boolean>; // C = number

// 应用到函数
function getValue<T>(value: T): StringOrNumber<T> {
  if (typeof value === 'string') {
    return value as StringOrNumber<T>;
  } else {
    return 0 as StringOrNumber<T>;
  }
}

const strValue = getValue('hello'); // strValue: string
const numValue = getValue(123); // numValue: number
const boolValue = getValue(true); // boolValue: number

8.4.2 条件类型与泛型工具类型结合

TypeScript 内置的泛型工具类型(如ExcludeExtractReturnType 等)本质上是基于条件类型实现的。


// 1. Exclude<T, U>:从 T 中排除可以赋值给 U 的类型(基于条件类型)
type T1 = Exclude<'a' | 'b' | 'c', 'a'>; // T1 = 'b' | 'c'(排除 'a')
type T2 = Exclude<number | string | boolean, number>; // T2 = string | boolean

// 2. Extract<T, U>:从 T 中提取可以赋值给 U 的类型(基于条件类型)
type T3 = Extract<'a' | 'b' | 'c', 'a' | 'd'>; // T3 = 'a'(提取与 U 重叠的类型)
type T4 = Extract<number | string | boolean, string | number>; // T4 = number | string

// 3. ReturnType<T>:获取函数 T 的返回值类型(基于条件类型)
type Func1 = () => string;
type Func2 = (a: number, b: number) => number;

type R1 = ReturnType<Func1>; // R1 = string(Func1 的返回值类型)
type R2 = ReturnType<Func2>; // R2 = number(Func2 的返回值类型)

8.5 映射类型(Mapped Types)

映射类型用于“基于已有类型,通过映射规则生成新类型”,核心是遍历已有类型的属性,对每个属性进行修改(如变为可选、只读、修改类型等)。映射类型的语法:{ [P in K]: T },其中 in 是映射操作符,K 是属性键的联合类型。

8.5.1 基本映射类型


interface User {
  id: number;
  name: string;
  age: number;
}

// 映射类型:将 User 的所有属性变为只读(类似 Readonly<User>)
type ReadonlyUser = {
  readonly [P in keyof User]: User[P];
};

const user: ReadonlyUser = { id: 1, name: '张三', age: 18 };
// 错误:无法修改只读属性
// user.name = '李四'; // 无法分配到 "name" ,因为它是只读属性

// 映射类型:将 User 的所有属性变为可选(类似 Partial<User>)
type PartialUser = {
  [P in keyof User]?: User[P];
};

const partialUser: PartialUser = { name: '张三' }; // 正确,可缺少属性

8.5.2 映射类型修改属性类型

映射时可修改属性的类型,生成新的类型结构。


interface User {
  id: number;
  name: string;
  age: number;
}

// 映射类型:将 User 的所有属性类型改为 string
type StringUser = {
  [P in keyof User]: string;
};

const stringUser: StringUser = {
  id: '1', // 原类型 number → 新类型 string
  name: '张三',
  age: '18' // 原类型 number → 新类型 string
};

// 映射类型:将 User 的所有属性类型改为 Promise<T>
type PromiseUser = {
  [P in keyof User]: Promise<User[P]>;
};

const promiseUser: PromiseUser = {
  id: Promise.resolve(1),
  name: Promise.resolve('张三'),
  age: Promise.resolve(18)
};

第九章 模块(Module)

模块是 TypeScript/JavaScript 中组织代码的核心方式,用于将代码分割为多个独立的文件(或模块),实现代码复用、避免命名冲突。TypeScript 完全支持 ES6 模块规范(import/export),同时也兼容 CommonJS 模块规范(require/module.exports)。

9.1 模块的基本概念

  • 模块内的变量、函数、类、接口等默认是“模块作用域”,仅在模块内部可访问;

  • 需通过 export 导出模块内的成员,其他模块通过 import 导入后才能访问;

  • 每个 TypeScript/JavaScript 文件都是一个独立的模块(除非设置了 module: 'none')。

9.2 模块的导出(Export)

模块的导出分为两种方式:命名导出(Named Export)和默认导出(Default Export)。

9.2.1 命名导出

命名导出用于导出多个成员,导出时需指定成员名称,导入时需使用相同的名称(可重命名)。


// 模块:user.ts
// 方式1:直接导出
export const name = '张三';
export const age = 18;

// 方式2:先定义后导出(批量导出)
const id = 1;
interface User {
  id: number;
  name: string;
  age: number;
}
export { id, User };

// 方式3:导出时重命名
export { id as userId, User as UserType };

9.2.2 默认导出

默认导出用于导出单个核心成员,每个模块只能有一个默认导出,导入时可自定义名称。


// 模块:user.ts
// 方式1:直接默认导出
export default class User {
  id: number;
  name: string;
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

// 方式2:先定义后默认导出
// class User { ... }
// export default User;

// 注意:默认导出可省略名称
export default function (a: number, b: number): number {
  return a + b;
}

9.3 模块的导入(Import)

根据导出方式的不同,导入方式分为命名导入和默认导入。

9.3.1 命名导入

导入命名导出的成员时,需使用 {} 包裹成员名称,名称需与导出时一致(可重命名)。


// 导入 user.ts 中的命名导出成员
import { name, age, id, User } from './user';

console.log(name); // 张三
console.log(age); // 18
console.log(id); // 1

const user: User = { id: 2, name: '李四', age: 20 };

// 导入时重命名(避免命名冲突)
import { name as userName, User as UserType } from './user';
console.log(userName); // 张三
const user2: UserType = { id: 3, name: '王五', age: 22 };

// 导入所有命名导出成员(批量导入)
import * as UserModule from './user';
console.log(UserModule.name); // 张三
console.log(UserModule.age); // 18
const user3: UserModule.User = { id: 4, name: '赵六', age: 24 };

9.3.2 默认导入

导入默认导出的成员时,无需使用 {},可自定义成员名称。


// 导入 user.ts 中的默认导出(User 类)
import User from './user';
const user = new User(1, '张三');

// 自定义名称导入
import MyUser from './user';
const user2 = new MyUser(2, '李四');

// 同时导入默认导出和命名导出
import User, { name, age } from './user';
const user3 = new User(3, name);
console.log(age); // 18

9.4 模块解析(Module Resolution)

模块解析是指 TypeScript 编译器根据导入路径(如 ./userreact)查找对应的模块文件的过程。TypeScript 支持两种模块解析策略:

  1. Classic:传统解析策略(默认用于 module: &#39;AMD&#39;/&#39;System&#39;/&#39;ES2015&#39; 等);

  2. Node:模仿 Node.js 的 CommonJS 解析策略(默认用于 module: 'CommonJS')。

可通过 tsconfig.json 中的 moduleResolution 选项指定解析策略:


{
  "compilerOptions": {
    "moduleResolution": "Node" // 指定模块解析策略为 Node
  }
}

9.4.1 相对路径导入

导入本地模块时使用相对路径(以 ./../ 开头),解析时从当前文件所在目录出发查找模块。


// 导入当前目录下的 user.ts
import { User } from './user';

// 导入上级目录下的 utils.ts
import { formatDate } from '../utils';

// 导入子目录下的 api/login.ts
import { login } from './api/login';

9.4.2 非相对路径导入

导入第三方库(如 reactlodash)或全局模块时使用非相对路径,解析时从 node_modules 目录或 tsconfig.json 配置的 baseUrlpaths 等路径查找模块。


// 导入第三方库(从 node_modules 查找)
import React from 'react';
import _ from 'lodash';

// 导入全局模块(需配置 baseUrl 和 paths)
// tsconfig.json 配置:
// {
//   "compilerOptions": {
//     "baseUrl": "./src",
//     "paths": {
//       "@/*": ["*"] // 映射 @ 为 src 目录
//     }
//   }
// }

// 使用 @ 导入 src 目录下的 utils.ts
import { formatDate } from '@/utils';

9.5 模块的类型声明

当导入第三方库(如无类型定义的 JavaScript 库)时,TypeScript 无法识别其类型,需手动编写类型声明文件(.d.ts)来提供类型信息。

9.5.1 类型声明文件的作用

  • 为 JavaScript 模块提供类型信息,实现 TypeScript 的类型检查;

  • 不生成 JavaScript 代码,仅用于类型推断和检查;

  • 常见的类型声明文件来源:第三方库自带(如 react)、@types 包(如 @types/lodash)、自定义 .d.ts 文件。

9.5.2 自定义类型声明文件


// 自定义类型声明文件:global.d.ts(全局类型声明)
declare module 'my-utils' { // 声明模块 my-utils
  // 导出模块的类型
  export function formatDate(date: Date): string;
  export function getRandomNumber(min: number, max: number): number;
  export const version: string;
}

// 导入模块并使用(TypeScript 可识别类型)
import { formatDate, getRandomNumber } from 'my-utils';

const now = new Date();
console.log(formatDate(now)); // 类型检查通过
console.log(getRandomNumber(1, 100)); // 类型检查通过

9.5.3 安装 @types 类型声明包

对于大多数常用的 JavaScript 库,社区已提供了对应的类型声明包(放在 @types 命名空间下),可通过 npm 安装:


# 安装 lodash 的类型声明包
npm install @types/lodash --save-dev

# 安装 jquery 的类型声明包
npm install @types/jquery --save-dev

第十章 命名空间(Namespace)

命名空间(Namespace)是 TypeScript 早期用于组织代码、避免命名冲突的方式,类似于“模块的简化版”。随着 ES6 模块规范的普及,命名空间的使用场景逐渐减少,目前主要用于:

  1. 在单个文件内组织代码,避免全局命名冲突;

  2. 与旧版 TypeScript 代码兼容;

  3. 简单的代码封装(不涉及跨文件导入导出)。

10.1 命名空间的基本使用

使用 namespace 关键字定义命名空间,通过 export 导出命名空间内的成员,外部通过 命名空间.成员名 访问。


// 定义命名空间 MathUtils
namespace MathUtils {
  // 内部成员(未导出,仅命名空间内可访问)
  const PI = 3.1415926;

  // 导出成员(外部可访问)
  export function circleArea(radius: number): number {
    return PI * radius * radius;
  }

  export function rectangleArea(width: number, height: number): number {
    return width * height;
  }
}

// 访问命名空间内的导出成员
console.log(MathUtils.circleArea(5)); // 78.539815(圆的面积)
console.log(MathUtils.rectangleArea(10, 20)); // 200(矩形的面积)

// 错误:无法访问未导出的内部成员
// console.log(MathUtils.PI); // 属性“PI”在类型“typeof MathUtils”上不存在

10.2 命名空间的嵌套

命名空间支持嵌套,可实现更细粒度的代码组织。


// 外层命名空间 Utils
namespace Utils {
  // 内层命名空间 Math
  export namespace Math {
    export function add(a: number, b: number): number {
      return a + b;
    }

    export function subtract(a: number, b: number): number {
      return a - b;
    }
  }

  // 内层命名空间 String
  export namespace String {
    export function toUpperCase(str: string): string {
      return str.toUpperCase();
    }

    export function trim(str: string): string {
      return str.trim();
    }
  }
}

// 访问嵌套命名空间的成员
console.log(Utils.Math.add(10, 20)); // 30
console.log(Utils.String.toUpperCase('hello')); // HELLO
console.log(Utils.String.trim('  world  ')); // world

10.3 命名空间与模块的区别

命名空间和模块都可用于组织代码、避免命名冲突,但核心区别如下:

  1. 文件关联:模块是“文件级”的(一个文件一个模块);命名空间是“文件内”的(可在单个文件内定义多个命名空间);

  2. 导入导出:模块通过 import/export 跨文件导入导出;命名空间主要在单个文件内使用,跨文件需通过 /// <reference path="..." /> 引用(繁琐);

  3. 使用场景:模块适合大型项目的跨文件代码组织;命名空间适合单个文件内的代码封装或旧代码兼容。


// 模块跨文件导入导出(推荐)
// math.ts(模块)
export function add(a: number, b: number): number {
  return a + b;
}

// main.ts(模块)
import { add } from './math';
console.log(add(10, 20)); // 30

// 命名空间跨文件引用(繁琐,不推荐)
// math.namespace.ts
namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }
}

// main.namespace.ts
/// <reference path="math.namespace.ts" /> // 引用命名空间文件
console.log(MathUtils.add(10, 20)); // 30

10.4 命名空间的合并

同名的命名空间会自动合并,合并后包含所有命名空间的导出成员。


// 命名空间 MathUtils 1
namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }
}

// 命名空间 MathUtils 2(同名,自动合并)
namespace MathUtils {
  export function subtract(a: number, b: number): number {
    return a - b;
  }
}

// 访问合并后的成员
console.log(MathUtils.add(10, 20)); // 30
console.log(MathUtils.subtract(10, 20)); // -10

第十一章 TypeScript 实践与工程化

掌握 TypeScript 的基础语法和高级特性后,需结合实际项目场景进行工程化配置和实践,提升开发效率和代码质量。本章将讲解 TypeScript 项目的工程化配置、构建工具集成、代码规范、测试实践,以及与主流前端框架(React/Vue/Angular)的结合方式,帮助你快速将 TypeScript 应用到实际项目中。

11.1 tsconfig.json 详细配置(核心)

tsconfig.json 是 TypeScript 项目的核心配置文件,用于指定编译器选项、项目文件范围等。前文已初步提及,本节将详细讲解常用配置项及最佳实践。

11.1.1 核心 compilerOptions 配置

compilerOptions 用于配置 TypeScript 编译器的行为,以下是高频配置项说明:


{
  "compilerOptions": {
    // 1. 目标 JavaScript 版本(ES3/ES5/ES6/ES2020/ESNext 等)
    "target": "ES2020", 
    // 2. 模块系统(CommonJS/ES6/ESNext/UMD/AMD 等)
    "module": "ESNext", 
    // 3. 模块解析策略(Node/Classic),推荐 Node(兼容 Node.js 模块机制)
    "moduleResolution": "Node", 
    // 4. 输出目录(编译后的 JavaScript 文件存放位置)
    "outDir": "./dist", 
    // 5. 输入目录(指定需要编译的 TypeScript 文件目录,与 include 配合使用)
    "rootDir": "./src", 
    // 6. 开启严格模式(强制开启所有严格类型检查选项,推荐开启)
    "strict": true, 
    // 7. 允许隐式 any 类型(strict 为 true 时自动为 false,不推荐开启)
    "noImplicitAny": false, 
    // 8. 检查函数参数是否有隐式的 any 类型
    "noImplicitParameters": true, 
    // 9. 检查 this 指向是否为 any 类型
    "noImplicitThis": true, 
    // 10. 确保函数有明确的返回值类型(无返回值需显式声明 void)
    "noImplicitReturns": true, 
    // 11. 严格检查 null/undefined(禁止将 null/undefined 赋值给其他类型,推荐开启)
    "strictNullChecks": true, 
    // 12. 严格检查函数参数的类型兼容性
    "strictFunctionTypes": true, 
    // 13. 严格检查属性初始化(类的属性必须在构造函数中初始化或设置默认值)
    "strictPropertyInitialization": true, 
    // 14. 允许导入 JSON 文件(需配合 resolveJsonModule 使用)
    "resolveJsonModule": true, 
    // 15. 生成 sourceMap(便于调试 TypeScript 代码,开发环境推荐开启)
    "sourceMap": true, 
    // 16. 生成声明文件(.d.ts,用于给 JavaScript 项目提供类型提示,库开发必备)
    "declaration": true, 
    // 17. 声明文件输出目录(默认与 outDir 一致,可单独指定)
    "declarationDir": "./dist/types", 
    // 18. 忽略声明文件中的错误(库开发时可能需要)
    "declarationMap": true, 
    // 19. 允许使用 ES 模块的导入/导出语法
    "esModuleInterop": true, 
    // 20. 禁止生成未使用的代码
    "removeComments": true, 
    // 21. 编译时不生成输出文件(仅做类型检查,用于 CI/CD 校验)
    "noEmit": false, 
    // 22. 指定需要包含的类型声明文件(如 @types/node、@types/react 等)
    "types": ["node", "react"], 
    // 23. 基础路径(用于非相对路径导入,配合 paths 实现别名映射)
    "baseUrl": "./src", 
    // 24. 路径别名映射(解决长路径导入问题,如 @/utils 映射到 src/utils)
    "paths": {
      "@/*": ["*"]
    }
  },
  // 25. 需要编译的文件/目录(支持通配符)
  "include": ["./src/**/*"], 
  // 26. 需要排除的文件/目录(编译时忽略)
  "exclude": ["./node_modules", "./dist", "./src/**/*.test.ts"], 
  // 27. 强制包含的文件(即使被 exclude 排除,也会编译)
  "files": ["./src/index.ts"] 
}

11.1.2 不同环境的配置优化

实际开发中需区分开发环境(dev)和生产环境(prod),可通过多个 tsconfig 文件实现:

  1. 基础配置(tsconfig.base.json):存放公共配置,供其他配置文件继承;

  2. 开发环境(tsconfig.dev.json):开启 sourceMap、不压缩代码、开启详细错误提示;

  3. 生产环境(tsconfig.prod.json):关闭 sourceMap、开启代码压缩、生成声明文件。


// tsconfig.dev.json(继承基础配置)
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "sourceMap": true,
    "noEmit": false,
    "removeComments": false
  },
  "include": ["./src/**/*"],
  "exclude": ["./src/**/*.test.ts"]
}

// tsconfig.prod.json(继承基础配置)
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "sourceMap": false,
    "declaration": true,
    "removeComments": true
  },
  "include": ["./src/**/*"],
  "exclude": ["./node_modules", "./dist", "./src/**/*.test.ts"]
}

11.2 构建工具集成

TypeScript 代码需编译为 JavaScript 才能在浏览器/Node.js 中运行,常用的构建工具包括 Vite、Webpack、Rollup 等,以下介绍主流工具的集成方式。

11.2.1 Vite 集成 TypeScript(推荐)

Vite 原生支持 TypeScript,无需复杂配置,开箱即用,适合中小型前端项目。

步骤 1:创建 Vite + TypeScript 项目

# npm
npm create vite@latest my-ts-project -- --template react-ts  # React + TS
# 或 Vue + TS
npm create vite@latest my-ts-project -- --template vue-ts
# 或 vanilla TS(无框架)
npm create vite@latest my-ts-project -- --template vanilla-ts

# 进入项目目录
cd my-ts-project
# 安装依赖
npm install
步骤 2:核心配置说明

Vite 会自动识别 tsconfig.json,如需自定义 TypeScript 编译行为,可在 vite.config.ts 中配置:


// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; // React 项目需引入
// import vue from '@vitejs/plugin-vue'; // Vue 项目需引入

export default defineConfig({
  plugins: [react()], // 启用 React 插件
  resolve: {
    // 路径别名(需与 tsconfig.json 中的 paths 一致)
    alias: {
      '@': '/src'
    }
  },
  // 自定义 TypeScript 编译选项(覆盖 tsconfig.json 中的配置)
  esbuild: {
    tsconfigRaw: `{
      "compilerOptions": {
        "strict": true,
        "target": "ES2020"
      }
    }`
  }
});

步骤 3:运行与构建

# 开发环境(热更新)
npm run dev

# 生产环境构建(输出到 dist 目录)
npm run build

# 预览构建产物
npm run preview

11.2.2 Webpack 集成 TypeScript

Webpack 需配合 ts-loader 或 babel-loader 处理 TypeScript 代码,适合复杂的大型项目。

步骤 1:安装依赖

# 安装核心依赖
npm install webpack webpack-cli webpack-dev-server --save-dev

# 安装 TypeScript 相关依赖
npm install typescript ts-loader --save-dev

# 若使用 Babel 兼容低版本浏览器,需额外安装
npm install @babel/core @babel/preset-env @babel/preset-typescript babel-loader --save-dev

步骤 2:配置 webpack.config.ts

// webpack.config.ts
import path from 'path';
import { Configuration } from 'webpack';

const config: Configuration = {
  entry: './src/index.ts', // 入口文件
  output: {
    filename: 'bundle.js', // 输出文件名
    path: path.resolve(__dirname, 'dist'), // 输出目录
    clean: true // 构建前清空 dist 目录
  },
  resolve: {
    extensions: ['.ts', '.js'], // 解析的文件后缀
    alias: {
      '@': path.resolve(__dirname, 'src') // 路径别名
    }
  },
  module: {
    rules: [
      // 方式 1:使用 ts-loader 处理 TypeScript
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/
      },
      // 方式 2:使用 babel-loader 处理(兼容低版本浏览器)
      // {
      //   test: /\.ts$/,
      //   use: {
      //     loader: 'babel-loader',
      //     options: {
      //       presets: [
      //         '@babel/preset-env',
      //         '@babel/preset-typescript'
      //       ]
      //     }
      //   },
      //   exclude: /node_modules/
      // }
    ]
  },
  devServer: {
    static: path.resolve(__dirname, 'dist'), // 静态文件目录
    port: 3000, // 开发服务器端口
    hot: true // 开启热更新
  },
  mode: process.env.NODE_ENV === 'production' ? 'production' : 'development' // 环境模式
};

export default config;

步骤 3:配置 package.json 脚本

{
  "scripts": {
    "dev": "webpack serve --mode development",
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development"
  }
}

11.3 代码规范与质量保障

工程化项目需统一代码规范,避免混乱,常用工具包括 ESLint(代码检查)、Prettier(代码格式化)、Husky(Git 钩子)等。

11.3.1 ESLint 配置(TypeScript 版)

ESLint 用于检查代码中的语法错误、逻辑错误和不规范写法,需配合 @typescript-eslint 插件支持 TypeScript。

步骤 1:安装依赖

npm install eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser --save-dev

步骤 2:创建 .eslintrc.js 配置文件

// .eslintrc.js
module.exports = {
  root: true, // 标识当前配置为根配置,不继承上层配置
  env: {
    browser: true, // 支持浏览器环境
    node: true, // 支持 Node.js 环境
    es2021: true // 支持 ES2021 语法
  },
  parser: '@typescript-eslint/parser', // 指定 TypeScript 解析器
  plugins: ['@typescript-eslint'], // 启用 TypeScript 插件
  extends: [
    'eslint:recommended', // 启用 ESLint 推荐规则
    'plugin:@typescript-eslint/recommended', // 启用 TypeScript 推荐规则
    'plugin:@typescript-eslint/strict-type-checked', // 启用严格类型检查规则
    'plugin:@typescript-eslint/stylistic-type-checked' // 启用代码风格检查规则
  ],
  parserOptions: {
    ecmaVersion: 'latest', // 支持最新 ES 语法
    sourceType: 'module', // 支持 ES 模块
    project: ['./tsconfig.json'] // 指定 tsconfig.json 路径(严格类型检查必需)
  },
  rules: {
    // 自定义规则(覆盖默认规则)
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // 忽略下划线开头的未使用参数
    '@typescript-eslint/explicit-function-return-type': 'off', // 关闭函数返回值类型必须显式声明的规则
    '@typescript-eslint/no-explicit-any': 'warn', // 禁止使用 any 类型,改为警告
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off' // 生产环境禁止 console
  }
};

步骤 3:创建 .eslintignore 文件(忽略无需检查的文件)

node_modules/
dist/
*.d.ts

11.3.2 Prettier 配置(代码格式化)

Prettier 用于统一代码格式化风格(如缩进、引号、分号等),与 ESLint 配合使用时需解决规则冲突。

步骤 1:安装依赖

npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev
# eslint-config-prettier:禁用 ESLint 中与 Prettier 冲突的规则
# eslint-plugin-prettier:将 Prettier 规则集成到 ESLint 中

步骤 2:创建 .prettierrc.js 配置文件

// .prettierrc.js
module.exports = {
  printWidth: 120, // 每行最大字符数
  tabWidth: 2, // 缩进宽度(2 个空格)
  useTabs: false, // 不使用制表符(使用空格)
  singleQuote: true, // 使用单引号
  semi: true, // 语句结尾加分号
  trailingComma: 'all', // 对象/数组末尾添加逗号
  bracketSpacing: true, // 对象字面量的大括号之间加空格({ foo: bar })
  arrowParens: 'always', // 箭头函数参数必须加括号((x) => x)
  proseWrap: 'never', // 不换行(针对 Markdown 文件)
  endOfLine: 'auto' // 自动识别换行符(LF/CRLF)
};

步骤 3:创建 .prettierignore 文件(忽略无需格式化的文件)

node_modules/
dist/
*.d.ts
package-lock.json
yarn.lock

步骤 4:整合 ESLint 与 Prettier

修改 .eslintrc.js,添加 Prettier 相关配置:


extends: [
  'eslint:recommended',
  'plugin:@typescript-eslint/recommended',
  'plugin:@typescript-eslint/strict-type-checked',
  'plugin:@typescript-eslint/stylistic-type-checked',
  'prettier', // 禁用 ESLint 与 Prettier 冲突的规则
  'plugin:prettier/recommended' // 启用 Prettier 规则,将格式化错误视为 ESLint 错误
],

11.3.3 Husky + lint-staged(Git 提交校验)

Husky 用于配置 Git 钩子,在代码提交(commit)或推送(push)前执行校验(如 ESLint 检查、Prettier 格式化),确保提交的代码符合规范。

步骤 1:安装依赖

npm install husky lint-staged --save-dev
# lint-staged:仅对暂存区(staged)的文件执行校验,提升效率

步骤 2:初始化 Husky

npx husky install
# 在 package.json 中添加脚本,确保项目安装依赖后自动启用 Husky
npm pkg set scripts.prepare="husky install"

步骤 3:添加 pre-commit 钩子(提交前校验)

npx husky add .husky/pre-commit "npx lint-staged"
步骤 4:配置 lint-staged(package.json 中添加)

{
  "lint-staged": {
    "*.{ts,js}": [
      "eslint --fix", // 自动修复 ESLint 错误
      "prettier --write" // 自动格式化代码
    ],
    "*.{json,md}": [
      "prettier --write" // 格式化 JSON 和 Markdown 文件
    ]
  }
}

此时,当执行 git commit 时,会自动对暂存区的 TypeScript/JavaScript 文件进行 ESLint 检查和 Prettier 格式化,若存在无法自动修复的错误,提交会失败。

11.4 TypeScript 测试实践

TypeScript 项目的测试工具常用 Jest(通用测试框架)+ React Testing Library(React 组件测试)/Vue Test Utils(Vue 组件测试),以下以 Jest 为例讲解基础配置。

11.4.1 Jest 集成 TypeScript

步骤 1:安装依赖

npm install jest ts-jest @types/jest --save-dev
# ts-jest:Jest 的 TypeScript 预处理器,用于编译 TypeScript 测试代码
# @types/jest:Jest 的类型声明文件

步骤 2:初始化 Jest 配置

npx ts-jest config:init
# 自动生成 jest.config.ts 文件

步骤 3:配置 jest.config.ts

// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest', // 使用 ts-jest 预设
  testEnvironment: 'node', // 测试环境(node/browser/jsdom)
  testMatch: ['**/__tests__/**/*.+(ts|js)', '**/?(*.)+(spec|test).+(ts|js)'], // 测试文件匹配规则
  moduleFileExtensions: ['ts', 'js', 'json', 'node'], // 模块文件后缀
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1' // 路径别名映射(与 tsconfig.json 一致)
  },
  collectCoverage: true, // 生成测试覆盖率报告
  coverageDirectory: 'coverage', // 覆盖率报告输出目录
  coverageReporters: ['text', 'lcov', 'clover'], // 覆盖率报告格式
  coveragePathIgnorePatterns: ['/node_modules/', '/dist/'] // 忽略覆盖率统计的目录
};

export default config;

步骤 4:编写测试用例

在 src 目录下创建 tests 文件夹,编写测试文件(如 math.test.ts):


// src/__tests__/math.test.ts
import { add, subtract } from '../utils/math';

describe('math 工具函数', () => {
  test('add 函数:1 + 2 应等于 3', () => {
    expect(add(1, 2)).toBe(3);
  });

  test('subtract 函数:5 - 3 应等于 2', () =&gt; {
    expect(subtract(5, 3)).toBe(2);
  });
});

步骤 5:运行测试

// package.json 中添加脚本
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch", // 监听文件变化,自动重新测试
    "test:coverage": "jest --coverage" // 生成测试覆盖率报告
  }
}

// 运行测试
npm test

11.5 TypeScript 与主流前端框架结合

TypeScript 已成为前端框架的标配,以下讲解与 React、Vue 3、Angular 的核心结合方式。

11.5.1 TypeScript + React

React 从 16.8 开始对 TypeScript 有良好支持,核心是给组件 Props、State、事件处理函数等添加类型约束。

1. 函数组件的类型定义

import React, { useState, useEffect } from 'react';

// 定义 Props 类型
interface ButtonProps {
  text: string;
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
  disabled?: boolean; // 可选属性
  type?: 'button' | 'submit' | 'reset'; // 字面量类型,限制可选值
}

// 函数组件:通过泛型指定 Props 类型
const Button: React.FC<ButtonProps> = ({
  text,
  onClick,
  disabled = false,
  type = 'button'
}) => {
  return (
    <button type={type} disabled={disabled} onClick={onClick}>
      {text}
    </button>
  );
};

// 父组件使用
const App: React.FC = () => {
  const [count, setCount] = useState<number>(0); // State 类型约束

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('点击事件', e);
    setCount(count + 1);
  };

  return (
    <div>
      <p>计数:{count}</p>
      <Button text="点击增加" onClick={handleClick} />
      <Button text="禁用按钮" disabled={true} onClick={handleClick} type="submit" />
    </div>
  );
};

export default App;
2. 类组件的类型定义(不推荐,优先函数组件)

import React, { Component } from 'react';

interface CounterProps {
  initialCount: number;
}

interface CounterState {
  count: number;
}

// 类组件:继承 Component 泛型,指定 Props 和 State 类型
class Counter extends Component<CounterProps, CounterState> {
  // 初始化 State
  state: CounterState = {
    count: this.props.initialCount
  };

  increment = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };

  render() {
    return (
      <div>
        <p>计数:{this.state.count}</p>
        <button onClick={this.increment}>增加</button>
      </div>
    );
  }
}

// 使用组件
<Counter initialCount={0} />

11.5.2 TypeScript + Vue 3

Vue 3 基于 TypeScript 重构,对 TypeScript 支持更完善,推荐使用

1. 组件 Props 类型定义(defineProps)

<script setup lang="ts">
// 方式 1:使用泛型定义 Props 类型
const props = defineProps<{
  title: string;
  count?: number;
  type: 'primary' | 'success' | 'danger';
}>();

// 方式 2:使用接口定义 Props 类型(更复杂的场景)
interface CardProps {
  title: string;
  desc?: string;
  onClose: () => void;
}

const props = defineProps<CardProps>();

// 定义 Emits 类型
const emit = defineEmits<{
  (e: 'click', id: number): void;
  (e: 'close'): void;
}>();

// 调用 Emit
const handleClick = () => {
  emit('click', 1);
  emit('close');
};

// 响应式变量类型定义
import { ref, reactive, computed } from 'vue';

const num = ref<number>(0); // 基本类型响应式变量
const user = reactive<{ name: string; age: number }>({ name: '张三', age: 18 }); // 对象类型响应式变量

// 计算属性类型定义
const doubleNum = computed<number>(() => num.value * 2);
</script>

<template>
  <div class="card">
    <h3>{{ props.title }}</h3>
    <p v-if="props.desc">{{ props.desc }}</p>
    <p>计数:{{ num }}</p>
    <p>两倍计数:{{ doubleNum }}</p>
    <button @click="handleClick">点击</button>
    <button @click="props.onClose">关闭</button>
  </div>
</template>

11.5.3 TypeScript + Angular

Angular 原生支持 TypeScript,所有组件、服务、模块等均使用 TypeScript 编写,核心是依赖注入和类型注解。

1. 组件类型定义

// src/app/components/hello/hello.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

// 定义接口
interface User {
  name: string;
  age: number;
}

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.css']
})
export class HelloComponent {
  // 输入属性(父组件传递数据),指定类型
  @Input() user: User = { name: '默认用户', age: 18 };
  @Input() title: string = '';

  // 输出属性(子组件向父组件传递数据),指定事件类型
  @Output() onUserChange = new EventEmitter<User>();

  // 方法类型定义
  changeUser(): void {
    const newUser: User = { name: '李四', age: 20 };
    this.onUserChange.emit(newUser); // 触发事件,传递数据
  }
}

2. 模板使用


<!-- hello.component.html -->
<h3>{{ title }}</h3>
<p>姓名:{{ user.name }},年龄:{{ user.age }}</p>
<button (click)="changeUser()">切换用户</button>

11.6 实战项目结构示例(React + TypeScript + Vite)

合理的项目结构能提升开发效率和可维护性,以下是中型 React + TypeScript 项目的推荐结构:


my-ts-project/
├── .eslintrc.js          # ESLint 配置
├── .prettierrc.js        # Prettier 配置
├── .husky/               # Husky 钩子配置
├── vite.config.ts        # Vite 配置
├── tsconfig.json         # TypeScript 基础配置
├── tsconfig.node.json    # Vite 相关的 TypeScript 配置
├── package.json
├── src/
│   ├── index.tsx         # 入口文件
│   ├── App.tsx           # 根组件
│   ├── api/              # 接口请求(API 封装、类型定义)
│   │   ├── user.ts       # 用户相关接口
│   │   ├── post.ts       # 文章相关接口
│   │   └── types/        # 接口返回数据类型定义
│   ├── components/       # 公共组件
│   │   ├── Button/       # 按钮组件(组件内聚:TSX + CSS + 类型定义)
│   │   │   ├── Button.tsx
│   │   │   ├── Button.module.css
│   │   │   └── Button.types.ts  # 组件 Props/State 类型定义
│   │   ├── Card/         # 卡片组件
│   │   └── ...
│   ├── hooks/            # 自定义 Hooks
│   │   ├── useRequest.ts # 请求 Hooks
│   │   ├── useAuth.ts    # 权限 Hooks
│   │   └── ...
│   ├── pages/            # 页面组件
│   │   ├── Home/         # 首页
│   │   ├── User/         # 用户页
│   │   │   ├── UserList.tsx
│   │   │   ├── UserDetail.tsx
│   │   │   └── ...
│   │   └── ...
│   ├── store/            # 状态管理(Redux/Vuex/Pinia)
│   │   ├── slices/       # Redux Toolkit 切片
│   │   ├── store.ts      # 状态管理初始化
│   │   └── types.ts      # 状态类型定义
│   ├── styles/           # 全局样式
│   │   ├── global.css    # 全局 CSS
│   │   └── variables.css # CSS 变量
│   ├── utils/            # 工具函数
│   │   ├── math.ts       # 数学工具
│   │   ├── format.ts     # 格式化工具
│   │   └── ...
│   ├── types/            # 全局类型定义(接口、类型别名)
│   │   ├── global.ts     # 全局类型
│   │   └── ...
│   └── assets/           # 静态资源(图片、字体等)
├── public/               # 公共静态资源(不被 Vite 处理)
├── __tests__/            # 测试文件(可选,也可放在 src 内)
└── dist/                 # 构建产物

11.7 TypeScript 工程化最佳实践总结

  1. 开启 strict: true 严格模式,强制类型检查,减少潜在 bug;

  2. 合理使用接口和类型别名,避免重复类型定义,提升代码复用性;

  3. 优先使用泛型工具类型(Partial/Required/Record 等),简化类型逻辑;

  4. 统一代码规范,通过 ESLint + Prettier + Husky 保障代码质量;

  5. 编写测试用例,通过 Jest 等工具提升代码可靠性;

  6. 结合框架特性使用 TypeScript(如 React 的 FC 泛型、Vue 3 的

第十二章 TypeScript 进阶技巧与性能优化

掌握基础和工程化实践后,本节将讲解 TypeScript 的进阶技巧(如类型编程、高级泛型应用)和性能优化方法,帮助你写出更优雅、高效的 TypeScript 代码。

12.1 高级泛型技巧

12.1.1 泛型条件类型的高级应用

泛型条件类型可实现复杂的类型推导,结合 infer 关键字可提取类型中的部分信息(如函数返回值类型、数组元素类型)。


// 1. 提取函数返回值类型(类似 ReturnType,但手动实现)
type GetReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;

type Func = (a: number, b: string) => boolean;
type FuncReturn = GetReturnType<Func>; // boolean

// 2. 提取数组元素类型
type GetArrayItemType<T extends any[]> = T extends (infer R)[] ? R : never;

type NumArray = number[];
type NumItem = GetArrayItemType<NumArray>; // number

type UserArray = { name: string }[];
type UserItem = GetArrayItemType<UserArray>; // { name: string }

// 3. 提取 Promise 包裹的类型
type UnwrapPromise<T extends Promise<any>> = T extends Promise<infer R> ? R : never;

type PromiseUser = Promise<{ name: string; age: number }>;
type User = UnwrapPromise<PromiseUser>; // { name: string; age: number }

12.1.2 泛型与映射类型的结合

结合泛型和映射类型可实现通用的类型转换工具,例如将对象的所有属性类型转换为可选、只读或其他类型。


// 1. 通用可选类型(类似 Partial,但手动实现)
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

interface User {
  id: number;
  name: string;
}

type PartialUser = MyPartial<User>; // { id?: number; name?: string }

// 2. 通用只读类型(类似 Readonly)
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

type ReadonlyUser = MyReadonly<User>; // { readonly id: number; readonly name: string }

// 3. 转换属性类型(将对象所有属性转换为 string 类型)
type MapToStr<T> = {
  [P in keyof T]: string;
};

type StrUser = MapToStr<User>; // { id: string; name: string }

12.2 类型编程技巧

12.2.1 类型守卫进阶

除了 typeofinstanceof,还可通过自定义类型守卫函数精准判断复杂类型。


interface User {
  id: number;
  name: string;
  isUser: true;
}

interface Admin {
  id: number;
  role: string;
  isAdmin: true;
}

type Person = User | Admin;

// 自定义类型守卫函数:判断是否为 User 类型
function isUser(person: Person): person is User {
  return (person as User).isUser !== undefined;
}

// 自定义类型守卫函数:判断是否为 Admin 类型
function isAdmin(person: Person): person is Admin {
  return (person as Admin).isAdmin !== undefined;
}

function printPersonInfo(person: Person) {
  if (isUser(person)) {
    console.log(`用户:${person.name}`); // 此时 person 被推断为 User 类型
  } else if (isAdmin(person)) {
    console.log(`管理员:${person.role}`); // 此时 person 被推断为 Admin 类型
  }
}

const user: User = { id: 1, name: '张三', isUser: true };
const admin: Admin = { id: 2, role: '超级管理员', isAdmin: true };

printPersonInfo(user); // 用户:张三
printPersonInfo(admin); // 管理员:超级管理员

12.2.2 利用类型断言简化复杂类型

在明确类型的场景下,合理使用类型断言可简化代码,但需避免滥用(否则会失去类型检查的意义)。


// 场景:从 API 获取数据,明确知道返回值类型
interface User {
  id: number;
  name: string;
  age: number;
}

async function fetchUser(): Promise<User> {
  const res = await fetch('/api/user');
  const data = await res.json();
  // 明确 data 是 User 类型,使用类型断言
  return data as User;
}

// 场景:处理 DOM 元素,明确元素类型
const input = document.getElementById('username') as HTMLInputElement;
input.value = '张三'; // 此时 input 被推断为 HTMLInputElement 类型,可直接访问 value 属性

12.3 TypeScript 性能优化

当项目规模较大时,TypeScript 编译速度可能下降,以下是常用的优化方法。

12.3.1 优化 tsconfig.json 配置

  1. 缩小 include/exclude 范围,只编译必要的文件;

  2. 关闭不必要的类型检查选项(如 noImplicitReturnsstrictPropertyInitialization,根据项目需求决定);

  3. 使用 incremental: true 开启增量编译(只重新编译修改过的文件,大幅提升二次编译速度);

  4. 使用 tsBuildInfoFile: "./dist/.tsbuildinfo" 指定增量编译信息文件的位置。


{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./dist/.tsbuildinfo",
    "include": ["./src/**/*"],
    "exclude": ["./node_modules", "./dist", "./src/**/*.test.ts"]
  }
}

12.3.2 拆分大型项目为多个子项目(TS Project References)

对于大型项目,可将代码拆分为多个子项目(如公共组件库、工具库、业务模块),通过 references 配置实现子项目依赖,编译时可并行编译子项目,提升效率。


// 根目录 tsconfig.json
{
  "compilerOptions": {
    "composite": true, // 启用项目组合(必须)
    "declaration": true,
    "outDir": "./dist"
  },
  "references": [
    { "path": "./packages/utils" }, // 依赖 utils 子项目
    { "path": "./packages/components" }, // 依赖 components 子项目
    { "path": "./packages/app" } // 主应用子项目
  ]
}

// 子项目 packages/utils/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["./src/**/*"]
}

12.3.3 减少不必要的类型复杂度

  1. 避免过度使用复杂的泛型和条件类型(尤其是在循环依赖的场景下);

  2. 对于复杂的类型,可拆分为多个简单的类型别名或接口,提升可读性和编译速度;

  3. 避免使用 any 类型(会增加类型检查负担),可用 unknown 替代并配合类型守卫。

12.3.4 利用构建工具优化

  1. 优先使用 Vite(基于 ESBuild,编译速度远快于 Webpack + ts-loader);

  2. Webpack 项目可使用 thread-loader 并行处理 TypeScript 编译,提升构建速度;

  3. 生产环境构建时,关闭 sourceMapdeclaration(若不需要类型声明文件)。

12.4 常见性能问题排查工具

  1. tsc --diagnostics:查看 TypeScript 编译的详细信息(如编译时间、文件数量、内存使用等);

  2. tsc --traceResolution:追踪模块解析过程,排查因模块解析缓慢导致的编译延迟;

  3. webpack-bundle-analyzer:分析 Webpack 构建产物的体积,排查因类型相关代码过多导致的产物过大问题。

第十三章 总结与展望

TypeScript 作为 JavaScript 的超集,通过静态类型检查为前端开发带来了更可靠的代码质量、更好的开发体验和可维护性。本章将总结 TypeScript 的核心价值,梳理学习路径,并展望其未来发展趋势。

13.1 TypeScript 核心价值总结

  1. 类型安全:静态类型检查提前发现潜在的语法错误、逻辑错误,减少运行时 bug,尤其适合中大型项目和团队协作;

  2. 开发体验提升:IDE 智能提示(如属性、方法、参数类型提示)、代码补全、重构支持,大幅提升开发效率;

  3. 代码可维护性:类型定义本身就是一种文档,清晰的类型约束让代码更易读、易理解、易重构,降低长期维护成本;

  4. 生态完善:主流前端框架(React/Vue/Angular)、构建工具(Vite/Webpack)、测试工具(Jest)均对 TypeScript 有良好支持,社区资源丰富;

  5. 向后兼容:TypeScript 兼容所有 JavaScript 代码,可逐步迁移现有 JavaScript 项目到 TypeScript,降低迁移成本。

13.2 TypeScript 学习路径梳理

  1. 基础阶段:掌握 TypeScript 基础类型(number/string/boolean 等)、变量声明(let/const)、函数类型(参数/返回值类型)、接口与类型别名的基本使用;

  2. 进阶阶段:学习联合类型、交叉类型、泛型、类型断言、类型守卫,掌握类与接口的结合、模块与命名空间的使用;

  3. 工程化阶段:配置 tsconfig.json、集成构建工具(Vite/Webpack)、搭建代码规范(ESLint/Prettier)、实现测试集成(Jest);

  4. 实战阶段:结合前端框架(React/Vue/Angular)开发项目,积累泛型高级应用、类型编程、性能优化的实战经验;

  5. 深入阶段:学习 TypeScript 编译器原理、自定义类型工具、参与开源项目的 TypeScript 类型定义贡献。

13.3 TypeScript 未来发展趋势

随着前端技术的不断演进,TypeScript 作为静态类型语言的代表,其发展方向将更贴合开发效率提升、跨平台能力增强和生态协同深化,主要呈现以下趋势:

  1. 与 ECMAScript 标准深度融合:TypeScript 始终紧跟 ECMAScript 标准,未来会持续快速支持 ES 新特性(如管道运算符、记录模式、模式匹配等),并优化这些特性的类型推导逻辑。同时,TypeScript 团队也在积极参与 ES 标准制定,推动类型相关特性在原生 JavaScript 中的探索,缩小 TypeScript 与原生 JS 的差距。

  2. 工具链与构建效率持续优化:针对大型项目编译速度慢的痛点,TypeScript 会进一步强化增量编译、并行编译能力,优化类型检查算法。同时,与 Vite、Turbopack 等新一代构建工具的协同会更紧密,借助 ESBuild、SWC 等高速编译器的能力,实现 TypeScript 代码的“极速编译”,提升开发体验。

  3. 跨平台与全栈支持增强:TypeScript 已成为全栈开发的主流选择,未来会进一步完善对 Node.js、Deno 等服务端环境的类型支持,优化对边缘计算、Serverless 等场景的适配。同时,在跨端开发领域(如 React Native、Electron、小程序),TypeScript 的类型定义会更完善,实现“一套代码、多端复用”的类型安全保障。

  4. AI 辅助开发深度集成:随着 AI 辅助开发工具(如 Copilot、CodeLlama)的普及,TypeScript 会进一步优化与 AI 工具的协同。例如,通过更清晰的类型元数据,帮助 AI 生成更精准的 TypeScript 代码;支持 AI 工具对类型错误的智能修复、复杂类型的自动推导,降低 TypeScript 的学习门槛。

  5. 类型系统的灵活性与严谨性平衡:TypeScript 会持续优化类型系统,在保持严谨性的同时提升灵活性。例如,完善对动态类型场景的支持(如更智能的 any 替代方案)、优化泛型的易用性、增强类型守卫的表现力,让开发者在享受类型安全的同时,无需过度编写繁琐的类型定义。

  6. 低代码/无代码平台的类型支持:随着低代码/无代码平台的兴起,TypeScript 会成为这类平台的核心类型支撑。通过标准化的类型定义,实现低代码组件的类型校验、数据流的类型安全保障,让低代码生成的代码也具备可维护性和扩展性,打通低代码与专业开发的鸿沟。

13.4 学习资源推荐

为帮助大家系统提升 TypeScript 能力,推荐以下优质学习资源,涵盖官方文档、经典书籍、在线课程、实战项目等多个维度:

13.4.1 官方文档与核心资源

  1. TypeScript 官方文档https://www.typescriptlang.org/docs/,最权威、最全面的学习资料,涵盖基础语法、高级特性、配置项等,建议优先阅读“Handbook”(手册)部分。

  2. TypeScript 中文网https://www.tslang.cn/docs/home.html,官方文档的中文翻译版本,适合英文基础较弱的开发者快速入门。

  3. TypeScript 官方 Playgroundhttps://www.typescriptlang.org/play,在线 TypeScript 运行环境,可实时查看代码编译后的 JavaScript 结果,适合测试小片段代码、验证类型逻辑。

13.4.2 经典书籍

  1. 《TypeScript 编程实战》:由 TypeScript 核心贡献者编写,深入讲解 TypeScript 的设计理念、核心特性和实战技巧,适合有一定 JS 基础的开发者进阶学习。

  2. 《深入浅出 TypeScript》:以通俗易懂的语言讲解 TypeScript 基础和进阶知识,配套大量实例,适合新手入门。

  3. 《Programming TypeScript》(英文版):详细剖析 TypeScript 类型系统的底层逻辑,适合希望深入理解 TypeScript 原理的开发者。

13.4.3 在线课程与视频教程

  1. TypeScript 官方入门课程(YouTube):由 TypeScript 团队出品,免费讲解基础语法和核心概念,适合英文听力较好的开发者。

  2. 掘金小册《TypeScript 全面进阶指南》:覆盖 TypeScript 基础、进阶特性、工程化实践、框架结合等内容,配套实战案例,适合中文开发者系统学习。

  3. B 站《TypeScript 从入门到精通》(尚硅谷/黑马程序员):免费的中文视频教程,内容全面、讲解细致,适合新手从零开始学习。

13.4.4 实战项目与练习平台

  1. TypeScript 官方示例仓库:https://github.com/microsoft/TypeScript-Node-Starter,包含 Node.js + TypeScript 的基础项目模板,可直接克隆学习和改造。

  2. LeetCode TypeScript 练习:在 LeetCode 上选择 JavaScript/TypeScript 语言刷题,通过算法练习巩固 TypeScript 类型定义和语法使用。

  3. 重构 JavaScript 项目:将自己或开源的 JavaScript 项目逐步重构为 TypeScript,在实战中解决类型定义、兼容性等问题,快速提升应用能力。

13.4.5 社区与资讯资源

  1. TypeScript 官方博客:https://devblogs.microsoft.com/typescript/,获取 TypeScript 最新更新、特性解读和团队动态。

  2. 掘金 TypeScript 专题:汇聚大量中文开发者的实战经验、问题解答和技术总结,适合日常查阅。

  3. Stack Overflow TypeScript 标签:https://stackoverflow.com/questions/tagged/typescript,解决学习和开发中遇到的具体问题。

  4. GitHub TypeScript 仓库:https://github.com/microsoft/TypeScript,关注源码更新、issues 讨论,了解 TypeScript 的开发进展和未来规划。

13.5 常见问题解答(FAQ)

在 TypeScript 学习和使用过程中,新手常遇到一些典型问题,以下是常见问题的解答和建议:

13.5.1 问题 1:何时使用 interface 还是 type alias?

两者在很多场景下可以互换,但核心区别和推荐用法如下:

  • 优先使用 interface:用于定义对象、类的结构(如组件 Props、API 返回数据结构),支持继承、合并,更符合面向对象的设计思想,IDE 对 interface 的类型提示更友好。

  • 使用 type alias:用于定义联合类型、交叉类型、泛型工具类型(如 type A = B | Ctype D = Partial<E>),支持基本类型的别名(如 type Age = number),不能合并。

总结:定义“结构型”类型用 interface,定义“组合型”或“基本类型别名”用 type alias

13.5.2 问题 2:如何避免过度使用 any 类型?

any 会关闭 TypeScript 的类型检查,过度使用会失去 TypeScript 的核心价值,建议通过以下方式替代:

  • unknown 替代:对于类型不确定的场景(如 API 返回数据、用户输入),优先使用 unknown,通过类型守卫或类型断言明确类型后再使用。

  • 定义精准的类型:即使是复杂对象,也应尽量编写完整的 interface 或 type alias,若部分属性类型不确定,可结合 PartialRecord 等工具类型简化定义。

  • 使用 as const 缩小类型范围:对于常量值,用 as const 让 TypeScript 推断出更精准的字面量类型(如 const status = 'success' as const)。

  • 临时使用 // @ts-ignore:若确实无法确定类型(如第三方库无类型定义),可使用 // @ts-ignore 临时忽略类型检查,并添加注释说明原因,后续尽量补充类型定义。

13.5.3 问题 3:泛型使用困难,如何快速上手?

泛型的核心是“类型参数化”,解决代码复用和类型安全的问题,新手可按以下步骤上手:

  • 从简单场景开始:先在工具函数中使用泛型(如数组去重、类型转换函数),理解泛型如何“捕获”传入的类型(如 function getFirst<T>(arr: T[]): T { return arr[0] })。

  • 学习常用泛型工具类型:先掌握官方提供的 PartialRequiredPickOmit 等工具类型的用法,理解它们的实现逻辑(本质是泛型 + 映射类型)。

  • 模仿实现简单泛型工具:尝试手动实现 MyPartialMyPick 等工具类型,加深对泛型和映射类型的理解。

  • 在框架中实践:在 React/Vue 项目中使用泛型定义组件 Props、状态类型(如 React 的 React.FC<Props>、Vue 的 defineProps<Props>),积累实战经验。

13.5.4 问题 4:TypeScript 编译速度慢,如何优化?

大型项目中 TypeScript 编译速度慢是常见问题,可通过以下方式优化(详细优化方案见 12.3 节):

  • 优化 tsconfig.json 配置:开启 incremental: true 增量编译、缩小 include/exclude 范围、关闭不必要的严格检查选项。

  • 拆分大型项目为子项目:使用 TS Project References 将项目拆分为多个子项目(如公共组件库、工具库、业务模块),实现并行编译。

  • 优化类型复杂度:避免过度使用复杂的泛型和条件类型,拆分复杂类型为多个简单类型,减少循环依赖的类型定义。

  • 选择高效的构建工具:优先使用 Vite(基于 ESBuild)替代 Webpack + ts-loader,或在 Webpack 中使用 thread-loader 并行编译。

13.5.5 问题 5:如何为第三方无类型的 JS 库添加类型支持?

对于没有提供类型定义的第三方 JavaScript 库,可通过以下方式添加类型支持:

  • 安装社区类型定义:优先查看 @types/[库名](如 @types/lodash),社区维护了大量常用库的类型定义,安装后即可直接使用。

  • 手动编写类型声明文件:若社区无相关类型定义,可在项目中创建 .d.ts 文件(如 custom.d.ts),手动编写库的类型定义(如函数、类、对象的类型)。

  • 临时使用 declare module:若无需精准类型检查,可通过 declare module '库名'临时声明模块,避免编译错误(如 declare module 'xxx-lib')。

13.6 结语

TypeScript 不仅是一门“带类型的 JavaScript”,更是一套提升前端开发效率、保障代码质量的解决方案。从基础语法到高级特性,从工程化配置到实战优化,TypeScript 的学习是一个循序渐进的过程,核心是理解“类型”的价值——通过静态类型检查提前规避错误,通过清晰的类型定义提升代码可维护性,让开发变得更高效、更可靠。

对于新手而言,不必追求一蹴而就,建议先掌握基础类型、接口、函数类型等核心知识点,在实际项目中逐步应用和积累;对于有经验的开发者,可深入学习泛型、类型编程、工程化优化等进阶内容,充分发挥 TypeScript 的强大能力。

随着前端技术的不断发展,TypeScript 的生态会越来越完善,其在全栈开发、跨平台开发、AI 辅助开发等领域的应用会更加广泛。持续学习和实践 TypeScript,不仅能提升个人技术能力,更能适应前端开发的未来趋势。

最后,希望本教程能为你提供清晰的学习路径和实用的技术指导,祝你在 TypeScript 的学习之路上不断进步,写出更优雅、更安全的代码!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐