承接上篇:掌握了基础类型后,我们来学习 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)); // 30
4.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 }; // ❌ 缺少 email
5.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); // ❌ protected
6.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); // ❌ private
6.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 | number
7.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>; // string
7.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" |
Extract<T, U> |
从 T 中提取可赋值给 U 的类型 | `Extract<"a" |
NonNullable<T> |
从 T 中排除 null 和 undefined | `NonNullable<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 的能力推向极致。