TypeScript接口与类型别名
接口(Interface)和类型别名(Type Alias)是TypeScript中用于定义复杂类型的两种主要方式。本文将详细介绍它们的用法、区别以及各自的最佳应用场景。
接口基础
接口定义
接口是TypeScript中描述对象形状的一种方式,它定义了对象应该具有的属性和方法:
typescript
// 基本接口定义
interface User {
id: number;
name: string;
email: string;
age?: number; // 可选属性
readonly createdAt: Date; // 只读属性
}
// 使用接口
const user: User = {
id: 1,
name: "张三",
email: "zhangsan@example.com",
createdAt: new Date()
};
// 错误:缺少必需属性
// const invalidUser: User = {
// name: "李四",
// email: "lisi@example.com"
// };
// 错误:尝试修改只读属性
// user.createdAt = new Date();
函数接口
接口也可以用来描述函数类型:
typescript
// 函数接口
interface SearchFunction {
(source: string, subString: string): boolean;
}
// 实现函数接口
const search: SearchFunction = function(source, subString) {
return source.includes(subString);
};
console.log(search("Hello, world", "world")); // true
可索引类型
接口可以描述可以通过索引访问的类型,如数组或对象:
typescript
// 数组索引接口
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = ["苹果", "香蕉", "橙子"];
console.log(myArray[0]); // "苹果"
// 对象索引接口
interface Dictionary {
[key: string]: string;
}
const dict: Dictionary = {
apple: "苹果",
banana: "香蕉",
orange: "橙子"
};
console.log(dict["apple"]); // "苹果"
// 混合索引类型
interface MixedDictionary {
[index: string]: string | number;
length: number; // 特定属性
name: string; // 特定属性
}
const mixed: MixedDictionary = {
name: "混合字典",
length: 3,
key1: "值1",
key2: 42
};
接口继承
接口可以继承其他接口,从而扩展其定义:
typescript
// 基础接口
interface Person {
name: string;
age: number;
}
// 继承单个接口
interface Employee extends Person {
employeeId: string;
department: string;
}
// 继承多个接口
interface Manager extends Employee, Admin {
subordinates: Employee[];
}
interface Admin {
privileges: string[];
}
// 使用继承的接口
const manager: Manager = {
name: "张经理",
age: 40,
employeeId: "EMP001",
department: "研发部",
privileges: ["系统管理", "用户管理"],
subordinates: [
{ name: "李工程师", age: 28, employeeId: "EMP002", department: "研发部" }
]
};
接口合并
TypeScript允许你多次声明同一个接口,这些声明会被合并:
typescript
// 接口合并
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
scale: number;
}
// 结果等同于:
// interface Box {
// height: number;
// width: number;
// length: number;
// scale: number;
// }
const box: Box = {
height: 10,
width: 20,
length: 30,
scale: 2
};
类型别名基础
类型别名定义
类型别名为现有类型创建新名称,可以用于任何类型,不仅仅是对象类型:
typescript
// 基本类型别名
type ID = string | number;
type Point = { x: number; y: number };
type Callback = (data: string) => void;
// 使用类型别名
const userId: ID = "user123";
const point: Point = { x: 10, y: 20 };
const handleData: Callback = (data) => console.log(data);
联合类型与交叉类型
类型别名常用于创建联合类型和交叉类型:
typescript
// 联合类型
type Status = "pending" | "fulfilled" | "rejected";
type Result<T> = T | Error;
let status: Status = "pending";
status = "fulfilled"; // 有效
// status = "completed"; // 错误:不能将类型"completed"分配给类型Status
// 交叉类型
type Person = {
name: string;
age: number;
};
type ContactInfo = {
email: string;
phone: string;
};
type Employee = Person & ContactInfo;
const employee: Employee = {
name: "张三",
age: 30,
email: "zhangsan@example.com",
phone: "123-456-7890"
};
泛型类型别名
类型别名可以使用泛型来创建可重用的类型定义:
typescript
// 泛型类型别名
type Container<T> = { value: T };
type Tree<T> = {
value: T;
left?: Tree<T>;
right?: Tree<T>;
};
// 使用泛型类型别名
const numberContainer: Container<number> = { value: 42 };
const stringContainer: Container<string> = { value: "Hello" };
const tree: Tree<number> = {
value: 1,
left: {
value: 2,
left: { value: 3 },
right: { value: 4 }
},
right: {
value: 5
}
};
条件类型
类型别名可以使用条件类型来基于条件创建不同的类型:
typescript
// 条件类型
type IsArray<T> = T extends any[] ? true : false;
type ExtractType<T> = T extends Array<infer U> ? U : never;
// 使用条件类型
type CheckString = IsArray<string>; // false
type CheckArray = IsArray<string[]>; // true
type NumberArray = number[];
type ExtractedType = ExtractType<NumberArray>; // number
// 更复杂的条件类型
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type NestedArray = number[][][];
type FlattenResult = Flatten<NestedArray>; // number
接口与类型别名的对比
语法差异
接口和类型别名在语法上有一些明显的差异:
typescript
// 接口定义
interface User {
name: string;
age: number;
}
// 类型别名定义
type User = {
name: string;
age: number;
};
功能差异
接口和类型别名在功能上也有一些重要区别:
typescript
// 接口可以被合并,类型别名不能
interface Box {
height: number;
}
interface Box {
width: number;
}
// Box现在有height和width属性
// 类型别名不能被合并
// 这会导致错误:重复标识符'Box'
// type Box = { height: number };
// type Box = { width: number };
// 类型别名可以为任何类型创建名称,包括原始类型、联合类型和元组
type Name = string;
type NameOrId = string | number;
type Coordinates = [number, number];
// 接口只能描述对象形状(包括函数和类)
interface Point {
x: number;
y: number;
}
// 类型别名可以使用计算属性
type Keys = 'firstname' | 'surname';
type Person = {
[key in Keys]: string;
};
// 等同于: { firstname: string; surname: string; }
// 接口不能表达映射类型
// 这是不可能的:
// interface Person {
// [key in Keys]: string;
// }
扩展方式
接口和类型别名的扩展方式不同:
typescript
// 接口扩展接口
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// 接口扩展类型别名
type Animal = {
name: string;
};
interface Dog extends Animal {
breed: string;
}
// 类型别名扩展接口
interface Animal {
name: string;
}
type Dog = Animal & {
breed: string;
};
// 类型别名扩展类型别名
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
};
实现与类
类可以实现接口或类型别名,但有一些限制:
typescript
// 类实现接口
interface Animal {
name: string;
makeSound(): void;
}
class Dog implements Animal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound(): void {
console.log("汪汪!");
}
}
// 类实现类型别名(如果类型别名描述的是对象形状)
type Animal = {
name: string;
makeSound(): void;
};
class Cat implements Animal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound(): void {
console.log("喵喵!");
}
}
// 类不能实现联合类型
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; size: number };
// 这会导致错误
// class Circle implements Shape {
// kind: 'circle' = 'circle';
// radius: number = 10;
// }
最佳实践
何时使用接口
接口在以下情况下是更好的选择:
- 当你需要定义对象、类或函数的形状时
- 当你需要声明可合并的API时
- 当你需要创建可被类实现的契约时
typescript
// 定义对象形状
interface User {
id: number;
name: string;
email: string;
}
// 定义类的契约
interface Repository<T> {
findAll(): Promise<T[]>;
findById(id: string): Promise<T | null>;
create(item: Omit<T, 'id'>): Promise<T>;
update(id: string, item: Partial<T>): Promise<T>;
delete(id: string): Promise<boolean>;
}
// 实现接口
class UserRepository implements Repository<User> {
async findAll(): Promise<User[]> {
// 实现代码...
return [];
}
async findById(id: string): Promise<User | null> {
// 实现代码...
return null;
}
async create(item: Omit<User, 'id'>): Promise<User> {
// 实现代码...
return { id: 1, ...item };
}
async update(id: string, item: Partial<User>): Promise<User> {
// 实现代码...
return { id: parseInt(id), name: '', email: '', ...item };
}
async delete(id: string): Promise<boolean> {
// 实现代码...
return true;
}
}
何时使用类型别名
类型别名在以下情况下是更好的选择:
- 当你需要使用联合类型、交叉类型、元组或其他复杂类型时
- 当你需要使用映射类型、条件类型等高级类型特性时
- 当你需要为函数签名或简单类型创建别名时
typescript
// 联合类型
type Result<T> = { success: true; value: T } | { success: false; error: Error };
function tryOperation<T>(operation: () => T): Result<T> {
try {
return { success: true, value: operation() };
} catch (error) {
return { success: false, error: error instanceof Error ? error : new Error(String(error)) };
}
}
// 映射类型
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
const config: Readonly<{ server: string; port: number }> = {
server: 'localhost',
port: 8080
};
// 条件类型
type ArrayElement<T> = T extends Array<infer U> ? U : never;
type Numbers = ArrayElement<number[]>; // number
混合使用
在许多情况下,接口和类型别名可以混合使用,以充分利用它们各自的优势:
typescript
// 使用类型别名定义基本类型
type ID = string | number;
type Status = 'active' | 'inactive' | 'pending';
// 使用接口定义对象形状
interface User {
id: ID;
name: string;
status: Status;
}
// 使用类型别名创建工具类型
type Nullable<T> = T | null;
type UserResponse = Nullable<User>;
// 接口继承类型别名
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
interface Post extends Timestamped {
id: ID;
title: string;
content: string;
authorId: ID;
}
高级用例
接口的递归定义
接口可以递归地引用自身,这对于定义树形结构或链表等数据结构很有用:
typescript
// 递归接口
interface TreeNode {
value: string;
children?: TreeNode[];
}
const tree: TreeNode = {
value: "根节点",
children: [
{
value: "子节点1",
children: [
{ value: "叶节点1" },
{ value: "叶节点2" }
]
},
{
value: "子节点2"
}
]
};
类型别名的递归定义
类型别名也可以递归,但需要特殊处理:
typescript
// 递归类型别名
type TreeNode = {
value: string;
children?: TreeNode[]; // 直接递归引用
};
// JSON值类型
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[] // 递归数组
| { [key: string]: JSONValue }; // 递归对象
const json: JSONValue = {
name: "张三",
age: 30,
isAdmin: false,
address: {
city: "北京",
zipCode: "100000"
},
hobbies: ["阅读", "游泳", "编程"]
};
接口与类型别名的高级映射
使用映射类型可以基于现有类型创建新类型:
typescript
// 使用接口定义基本类型
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 使用类型别名和映射类型创建派生类型
type PublicUser = Omit<User, 'password'>;
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;
// 创建具有特定属性的类型
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'password'
type UserValues = User[keyof User]; // number | string
// 条件映射
type OptionalToNullable<T> = {
[K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};
interface Options {
timeout?: number;
cache?: boolean;
debug?: boolean;
}
type NullableOptions = OptionalToNullable<Options>;
// 等同于: { timeout: number | null; cache: boolean | null; debug: boolean | null; }
品牌/名义类型
使用接口和类型别名可以创建"品牌类型",这是一种模拟名义类型系统的方式:
typescript
// 使用接口创建品牌类型
interface UserId extends String {
readonly _brand: unique symbol;
}
interface OrderId extends String {
readonly _brand: unique symbol;
}
// 创建品牌类型的值
function createUserId(id: string): UserId {
return id as unknown as UserId;
}
function createOrderId(id: string): OrderId {
return id as unknown as OrderId;
}
// 使用品牌类型
function processUser(id: UserId) {
console.log(`处理用户: ${id}`);
}
const userId = createUserId("user-123");
const orderId = createOrderId("order-456");
processUser(userId); // 有效
// processUser(orderId); // 错误:类型不兼容
// processUser("raw-string"); // 错误:类型不兼容
总结
TypeScript中的接口和类型别名是定义和组织类型的强大工具。虽然它们有很多重叠的功能,但各自也有独特的优势:
- 接口更适合定义对象形状、类契约和API,特别是当你需要声明合并或创建可实现的契约时。
- 类型别名更适合处理复杂类型组合、联合类型、交叉类型和高级类型操作。
在实际项目中,两者通常会结合使用,以充分利用TypeScript类型系统的强大功能。随着你对TypeScript的深入了解,你将能够更加熟练地选择最适合特定场景的类型定义方式。