承接上篇:掌握了基础类型后,我们来学习 TypeScript 中真正让代码变得可扩展、可复用的核心特性。
上篇我们学会了给变量、数组、对象标注类型。但如果你的代码只用到基础类型,那 TypeScript 的威力连十分之一都没发挥出来。真正让 TypeScript 与众不同的,是它能够描述函数的行为、定义对象的结构(接口)、实现面向对象编程(类)以及用高级类型进行类型编程。
本文将聚焦这些核心内容,全程纯 TypeScript,不涉及任何前端框架。
第四部分:函数——类型系统的“连接器”
函数是 JavaScript 的“一等公民”,TypeScript 为函数提供了丰富的类型注解方式。
4.1 函数参数与返回值的类型注解
最简单的函数类型注解包括参数类型和返回值类型:
typescript
// 语法:(参数名: 类型, 参数名: 类型): 返回值类型 function add(x: number, y: number): number { return x + y; } const result = add(3, 5); // result 被推断为 number如果函数没有返回值,使用void:
typescript
function logMessage(msg: string): void { console.log(msg); // 没有 return 语句,或 return undefined; }4.2 函数类型表达式
你可以用变量来存储函数,此时需要描述这个变量的类型:
typescript
// 定义一个函数类型:接受两个 number,返回 number let myAdd: (x: number, y: number) => number; // 赋值一个符合该类型的函数 myAdd = function (a: number, b: number): number { return a + b; }; // 也可以使用类型别名简化 type MathOperation = (a: number, b: number) => number; let multiply: MathOperation = (x, y) => x * y;4.3 可选参数和默认参数
TypeScript 中的每个函数参数都是必传的,除非标记为可选或提供默认值。
可选参数:使用?标记,必须放在必选参数的后面。
typescript
function buildName(firstName: string, lastName?: string): string { if (lastName) { return `${firstName} ${lastName}`; } return firstName; } console.log(buildName("Alice")); // "Alice" console.log(buildName("Alice", "Chen")); // "Alice Chen" // console.log(buildName("Alice", "Chen", "Jr")); // ❌ 参数过多默认参数:为参数提供默认值,此时参数自动变为可选。
typescript
function greet(name: string, greeting: string = "Hello"): string { return `${greeting}, ${name}!`; } console.log(greet("Bob")); // "Hello, Bob!" console.log(greet("Bob", "Hi")); // "Hi, Bob!"4.4 剩余参数
使用...rest: Type[]语法接收任意数量的参数:
typescript
function sumNumbers(...numbers: number[]): number { return numbers.reduce((total, num) => total + num, 0); } console.log(sumNumbers(1, 2, 3, 4, 5)); // 15 console.log(sumNumbers(10, 20)); // 304.5 函数重载
函数重载允许同一个函数根据不同的参数类型或数量,执行不同的逻辑,并提供准确的类型提示。
重载的语法:先写多个重载签名(只有类型,没有实现),然后写一个通用的实现签名。
typescript
// 重载签名 function formatValue(value: string): string; function formatValue(value: number): string; function formatValue(value: boolean): string; // 实现签名(必须兼容所有重载) function formatValue(value: string | number | boolean): string { if (typeof value === "string") { return `"${value}"`; } else if (typeof value === "number") { return value.toFixed(2); } else { return value ? "true" : "false"; } } console.log(formatValue("hello")); // 输出 "hello" console.log(formatValue(3.1415)); // 输出 3.14 console.log(formatValue(true)); // 输出 true注意:TypeScript 会根据你传入的参数类型,匹配到对应的重载签名,从而给出准确的返回类型。上面的例子中,formatValue("hi")的返回类型被推断为string。
4.6 函数中的this类型
在 JavaScript 中,this的指向容易出错。TypeScript 允许你在函数参数中声明this的类型(第一个参数,名字必须叫this)。
typescript
interface User { name: string; age: number; } function describe(this: User): void { console.log(`${this.name} is ${this.age} years old`); } const user: User = { name: "Alice", age: 30 }; describe.call(user); // 正确调用 // describe(); // ❌ 错误:this 上下文丢失通常你不需要手动标注this,但在事件监听器、类方法等场景中非常有用。
第五部分:接口——定义对象的形状
接口是 TypeScript 中最核心的概念之一,它用于定义对象的结构(即这个对象应该有哪些属性,每个属性的类型是什么)。
5.1 基本接口语法
typescript
interface Person { name: string; age: number; email: string; } // 使用接口 const alice: Person = { name: "Alice", age: 25, email: "alice@example.com" }; // 缺少属性会报错 // const bob: Person = { name: "Bob", age: 30 }; // ❌ 缺少 email5.2 可选属性和只读属性
可选属性:用
?标记,表示该属性可以不存在。只读属性:用
readonly标记,表示初始化后不可修改。
typescript
interface Product { readonly id: number; // 只读 name: string; price: number; description?: string; // 可选 } const phone: Product = { id: 1001, name: "Smartphone", price: 599 // description 可以省略 }; // phone.id = 1002; // ❌ 错误:id 是只读的 phone.price = 499; // ✅ 可以修改非只读属性5.3 多余属性检查
当将一个对象字面量直接赋值给接口类型的变量时,TypeScript 会进行多余属性检查:不允许出现接口中未定义的属性。
typescript
interface Point { x: number; y: number; } // 错误:对象字面量只能指定已知属性 // const p: Point = { x: 10, y: 20, z: 30 }; // ❌ 'z' 不在 Point 中 // 绕过多余属性检查的几种方式: // 1. 使用类型断言 const p1 = { x: 10, y: 20, z: 30 } as Point; // 2. 先赋值给另一个变量 const temp = { x: 10, y: 20, z: 30 }; const p2: Point = temp; // ✅ 没有直接使用字面量,不会触发多余属性检查 // 3. 使用索引签名(见下文)5.4 索引签名
当你不确定对象会有哪些属性,但知道属性值的类型时,可以使用索引签名。
typescript
// 字符串索引签名:所有属性值必须是 string interface StringDictionary { [key: string]: string; } const dict: StringDictionary = { hello: "你好", world: "世界" // age: 25 // ❌ 错误:数字不能赋给 string }; // 数字索引签名(通常用于类数组对象) interface NumberArray { [index: number]: string; } const arr: NumberArray = ["a", "b", "c"]; console.log(arr[0]); // "a"注意:同时存在字符串索引和数字索引时,数字索引的返回值类型必须是字符串索引返回值类型的子类型(因为 JavaScript 会将数字索引转换为字符串)。
5.5 函数类型接口
接口不仅可以描述对象,还可以描述函数类型:
typescript
interface GreetFunction { (name: string, greeting?: string): string; } const greet: GreetFunction = (name, greeting = "Hello") => { return `${greeting}, ${name}!`; };5.6 接口继承
接口可以继承另一个或多个接口,把它们的属性合并过来。
typescript
interface Animal { name: string; age: number; } interface Dog extends Animal { breed: string; bark(): void; } const myDog: Dog = { name: "Buddy", age: 3, breed: "Golden Retriever", bark() { console.log("Woof!"); } }; // 多继承 interface Swimmer { swim(): void; } interface Flyer { fly(): void; } interface Duck extends Swimmer, Flyer { quack(): void; }5.7 接口与类型别名的区别
回顾上篇我们讲了类型别名(type),它也能描述对象形状。什么时候用接口,什么时候用类型别名?
| 特性 | interface | type |
|---|---|---|
| 声明合并 | ✅ 支持(多次定义同名接口会自动合并) | ❌ 不支持 |
| 继承/扩展 | extends语法 | 交叉类型& |
| 描述对象/函数 | ✅ 专门为此设计 | ✅ 也可以 |
| 描述联合类型 | ❌ 不能 | ✅ 可以 |
| 描述元组 | ❌ 不能(TS 4.2+ 可以部分用,但不推荐) | ✅ 可以 |
| 实现(implements) | 类可以实现多个接口 | 类不能实现类型别名(但可以 implements 交叉类型) |
经验法则:默认使用interface,直到你需要使用type的独有特性(如联合类型、映射类型等)。
typescript
// 声明合并示例 interface User { name: string; } interface User { age: number; } // 最终 User 接口包含 name 和 age const u: User = { name: "Alice", age: 25 };第六部分:类——面向对象的 TypeScript
TypeScript 对 ES6 类进行了增强,加入了访问修饰符、抽象类、只读属性等面向对象特性。
6.1 基础类语法
typescript
class Animal { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } speak(): void { console.log(`${this.name} makes a sound.`); } } const cat = new Animal("Whiskers", 2); cat.speak();6.2 访问修饰符
TypeScript 提供了三个访问修饰符:
public:默认,任何地方都可访问。private:只能在类内部访问。protected:能在类内部及子类中访问,但不能在实例中访问。
typescript
class Person { public name: string; // 公开 private age: number; // 私有 protected id: number; // 受保护 constructor(name: string, age: number, id: number) { this.name = name; this.age = age; this.id = id; } public getAge(): number { return this.age; // 内部可以访问 private } } class Employee extends Person { private department: string; constructor(name: string, age: number, id: number, department: string) { super(name, age, id); this.department = department; } public getInfo(): string { // return this.age; // ❌ private,不能访问 return `${this.name} (ID: ${this.id})`; // ✅ protected 可以访问 } } const emp = new Employee("Bob", 30, 12345, "IT"); console.log(emp.name); // ✅ public // console.log(emp.age); // ❌ private // console.log(emp.id); // ❌ protected6.3 参数属性(Parameter Properties)
TypeScript 提供了一种简写:在构造函数参数前加上修饰符,可以自动声明并初始化同名字段。
typescript
class User { // 相当于声明了 public name: string,并在构造函数中赋值 constructor(public name: string, private age: number) {} } const u = new User("Alice", 25); console.log(u.name); // ✅ // console.log(u.age); // ❌ private6.4 只读属性
使用readonly关键字,属性只能在初始化时赋值,之后不可修改。
typescript
class Book { readonly isbn: string; title: string; constructor(isbn: string, title: string) { this.isbn = isbn; this.title = title; } } const book = new Book("123456", "TypeScript Guide"); // book.isbn = "654321"; // ❌ 错误 book.title = "New Title"; // ✅ 可以修改非 readonly 属性6.5 抽象类
抽象类不能直接实例化,必须被继承。它可以包含抽象方法(没有方法体,子类必须实现)和具体方法。
typescript
abstract class Shape { abstract getArea(): number; // 抽象方法,没有实现 describe(): string { // 具体方法,可以被子类继承 return `This shape has area: ${this.getArea()}`; } } class Circle extends Shape { constructor(private radius: number) { super(); } getArea(): number { return Math.PI * this.radius ** 2; } } class Square extends Shape { constructor(private side: number) { super(); } getArea(): number { return this.side ** 2; } } // const s = new Shape(); // ❌ 错误:抽象类不能实例化 const circle = new Circle(5); console.log(circle.getArea()); // 78.539... console.log(circle.describe()); // "This shape has area: 78.539..."6.6 类实现接口
类可以使用implements关键字来实现一个或多个接口,强制类包含接口中定义的所有属性/方法。
typescript
interface ClockInterface { currentTime: Date; setTime(d: Date): void; } class Clock implements ClockInterface { currentTime: Date; constructor(h: number, m: number) { this.currentTime = new Date(); } setTime(d: Date): void { this.currentTime = d; } }类也可以实现多个接口:
typescript
interface Printable { print(): void; } interface Serializable { serialize(): string; } class Document implements Printable, Serializable { print(): void { console.log("Printing..."); } serialize(): string { return "document data"; } }6.7 静态成员
静态属性和方法属于类本身,而不是实例。使用static关键字。
typescript
class MathUtils { static PI: number = 3.14159; static circleArea(radius: number): number { return this.PI * radius ** 2; } } console.log(MathUtils.PI); // 3.14159 console.log(MathUtils.circleArea(5)); // 78.53975第七部分:高级类型——类型编程的艺术
前面我们学了基础类型、函数、接口和类。这一部分将进入 TypeScript 的“高级玩法”:交叉类型、类型保护、类型操作符、映射类型、条件类型等。
7.1 交叉类型(&)
交叉类型将多个类型合并为一个类型,包含所有类型的成员。常用于混入(mixin)。
typescript
interface A { a: string; } interface B { b: number; } type C = A & B; // 既有 a 又有 b const obj: C = { a: "hello", b: 42 }; // 更复杂的例子:合并函数签名 type Admin = { name: string; privileges: string[] }; type Employee = { name: string; startDate: Date }; type ElevatedEmployee = Admin & Employee; const e: ElevatedEmployee = { name: "Alice", privileges: ["create-server"], startDate: new Date() };注意:交叉类型可能会产生不可调和的矛盾,比如string & number会变成never。
7.2 类型保护(Type Guards)
类型保护是运行时检查,用于在特定作用域内收窄类型。
typeof类型保护:
typescript
function padLeft(value: string | number, padding: number): string { if (typeof value === "string") { // 这里 value 是 string return value.padStart(padding, " "); } else { // 这里 value 是 number return value.toString().padStart(padding, " "); } }instanceof类型保护:
typescript
class Bird { fly() { console.log("Flying"); } } class Fish { swim() { console.log("Swimming"); } } function move(animal: Bird | Fish) { if (animal instanceof Bird) { animal.fly(); } else { animal.swim(); } }自定义类型保护:使用value is Type语法定义函数,返回布尔值,告诉 TS 类型收窄。
typescript
interface Cat { meow(): void; } interface Dog { bark(): void; } function isCat(animal: Cat | Dog): animal is Cat { return (animal as Cat).meow !== undefined; } function makeSound(animal: Cat | Dog) { if (isCat(animal)) { animal.meow(); // TS 知道这里是 Cat } else { animal.bark(); // TS 知道这里是 Dog } }7.3 类型操作符keyof
keyof操作符获取一个类型的所有属性名组成的联合类型。
typescript
interface Person { name: string; age: number; address: string; } type PersonKeys = keyof Person; // "name" | "age" | "address" function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const p: Person = { name: "Alice", age: 25, address: "123 St" }; const nameValue = getProperty(p, "name"); // string // const invalid = getProperty(p, "invalid"); // ❌ 错误7.4 索引访问类型(T[K])
你可以像访问数组元素一样,获取类型的某个属性的类型。
typescript
interface Car { brand: string; year: number; owner: { name: string; age: number }; } type BrandType = Car["brand"]; // string type OwnerType = Car["owner"]; // { name: string; age: number } type OwnerAgeType = Car["owner"]["age"]; // number // 联合索引 type Keys = "brand" | "year"; type Values = Car[Keys]; // string | number7.5 映射类型(Mapped Types)
映射类型基于旧类型创建新类型,语法:{ [P in K]: T[P] }。
内置的几个常用映射类型:
typescript
// Partial<T>:所有属性变为可选 type PartialPerson = Partial<Person>; // { name?: string; age?: number; address?: string; } // Readonly<T>:所有属性变为只读 type ReadonlyPerson = Readonly<Person>; // { readonly name: string; readonly age: number; readonly address: string; } // Pick<T, K>:选取部分属性 type NameAndAge = Pick<Person, "name" | "age">; // { name: string; age: number; } // Record<K, T>:创建一个对象类型,键为 K,值为 T type Page = "home" | "about" | "contact"; type PageInfo = Record<Page, { title: string; url: string }>; // 等价于: // { home: {title, url}; about: {...}; contact: {...} }你可以自己实现这些工具类型:
typescript
type MyPartial<T> = { [P in keyof T]?: T[P]; }; type MyReadonly<T> = { readonly [P in keyof T]: T[P]; };7.6 条件类型(Conditional Types)
条件类型的语法类似于三元运算符:T extends U ? X : Y。
typescript
type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<42>; // false // 使用 infer 提取类型 type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function example(): number[] { return [1, 2, 3]; } type R = ReturnType<typeof example>; // number[] // 递归条件类型 type Flatten<T> = T extends any[] ? T[number] : T; type NumArr = Flatten<number[]>; // number type Str = Flatten<string>; // string7.7 内置工具类型速查
TypeScript 提供了很多实用的工具类型,下面列举最常用的:
| 工具类型 | 作用 | 示例 |
|---|---|---|
Partial<T> | 将 T 的所有属性变为可选 | Partial<Person> |
Required<T> | 将 T 的所有属性变为必选 | Required<PartialPerson> |
Readonly<T> | 将 T 的所有属性变为只读 | Readonly<Person> |
Pick<T, K> | 从 T 中选取指定的属性集 K | Pick<Person, "name"> |
Omit<T, K> | 从 T 中排除指定的属性集 K | Omit<Person, "age"> |
Exclude<T, U> | 从 T 中排除可赋值给 U 的类型 | Exclude<"a"|"b", "a">→"b" |
Extract<T, U> | 从 T 中提取可赋值给 U 的类型 | Extract<"a"|"b", "a">→"a" |
NonNullable<T> | 从 T 中排除 null 和 undefined | NonNullable<string|null>→string |
ReturnType<T> | 获取函数 T 的返回值类型 | ReturnType<()=>number>→number |
Parameters<T> | 获取函数 T 的参数类型元组 | Parameters<(a: string)=>void>→[string] |
InstanceType<T> | 获取构造函数 T 的实例类型 | InstanceType<typeof Person>→Person |
示例用法:
typescript
interface Todo { title: string; description: string; completed: boolean; createdAt: Date; } // 创建一个只包含标题和完成状态的类型 type TodoPreview = Pick<Todo, "title" | "completed">; // 创建一个不包含描述的类型 type TodoWithoutDesc = Omit<Todo, "description">; // 将 Date 属性变为可选 type TodoWithOptionalDate = Partial<Pick<Todo, "createdAt">> & Omit<Todo, "createdAt">;小结与下篇预告
本文(中篇)涵盖了:
函数:参数、返回值、可选/默认参数、剩余参数、重载
接口:对象形状、可选/只读属性、索引签名、继承、与 type 的区别
类:访问修饰符、参数属性、抽象类、实现接口、静态成员
高级类型:交叉类型、类型保护、keyof、索引访问、映射类型、条件类型、内置工具类型
下篇(终篇)预告:
泛型的深入(泛型约束、泛型类、泛型与接口/类的结合)
模块与命名空间
声明文件(
d.ts)配置文件的深度解析
实际项目中的最佳实践与性能优化
如果你已经掌握了中篇的内容,你完全有能力用 TypeScript 编写出结构清晰、类型安全的程序。请继续关注下篇,我们将把 TypeScript 的能力推向极致。