TypeScript 最佳实践
概述
TypeScript 最佳实践是一系列经过验证的开发建议,可以帮助你编写更安全、更易维护、更高效的 TypeScript 代码。遵循这些实践可以让你充分利用 TypeScript 的类型系统,减少错误,提高代码质量。本文档涵盖了从类型定义到项目组织的各个方面。
类型定义最佳实践
优先使用接口而非类型别名(用于对象)
对于对象类型的定义,优先使用 interface,因为它支持声明合并,更适合扩展:
// ✅ 推荐:使用接口定义对象类型
interface User {
name: string;
age: number;
}
// 可以扩展
interface User {
email?: string; // 声明合并
}
// ❌ 不推荐:使用类型别名定义对象
type User = {
name: string;
age: number;
};提示
当需要联合类型、交叉类型或工具类型时,使用 type 更合适。
使用类型别名简化复杂类型
对于复杂的类型定义,使用类型别名可以提高可读性:
// ✅ 推荐:使用类型别名简化复杂类型
type EventHandler = (event: Event) => void;
type StringOrNumber = string | number;
type Nullable<T> = T | null;
// 使用
const onClick: EventHandler = (e) => {
console.log('clicked');
};避免使用 any,优先使用 unknown
any 会禁用类型检查,应该尽量避免使用。当需要表示"未知类型"时,使用 unknown:
// ❌ 不推荐:使用 any
function processData(data: any) {
return data.value; // 没有类型检查
}
// ✅ 推荐:使用 unknown
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return (data as { value: string }).value; // 需要类型守卫
}
throw new Error('Invalid data');
}使用字面量类型提高类型精确度
使用字面量类型可以让类型更加精确:
// ✅ 推荐:使用字面量类型
type Status = 'pending' | 'success' | 'error';
function handleStatus(status: Status) {
// 类型更精确,IDE 可以提供自动补全
}
// ❌ 不推荐:使用 string
function handleStatus(status: string) {
// 类型太宽泛,容易出错
}函数最佳实践
明确函数返回类型
即使 TypeScript 可以推断返回类型,明确声明返回类型可以提高代码可读性:
// ✅ 推荐:明确返回类型
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// ❌ 不推荐:依赖类型推断
function calculateTotal(items: Item[]) {
return items.reduce((sum, item) => sum + item.price, 0);
}使用函数重载而非联合类型参数
当函数有多种调用方式时,使用函数重载比联合类型更清晰:
// ✅ 推荐:使用函数重载
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
return String(value);
}
// ❌ 不推荐:使用联合类型
function format(value: string | number): string {
return String(value);
}使用可选参数而非重载(简单情况)
对于简单的可选参数,直接使用可选参数比函数重载更简洁:
// ✅ 推荐:使用可选参数
function greet(name: string, greeting?: string): string {
return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;
}
// ❌ 不推荐:过度使用重载
function greet(name: string): string;
function greet(name: string, greeting: string): string;
function greet(name: string, greeting?: string): string {
return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;
}类最佳实践
使用访问修饰符明确可见性
明确使用 public、private、protected 修饰符,提高代码可读性:
// ✅ 推荐:明确访问修饰符
class User {
public name: string; // 公开属性
private age: number; // 私有属性
protected email: string; // 受保护属性
constructor(name: string, age: number, email: string) {
this.name = name;
this.age = age;
this.email = email;
}
}
// ❌ 不推荐:依赖默认行为
class User {
name: string; // 默认是 public,但不明确
age: number;
}使用参数属性简化构造函数
当构造函数只是简单赋值时,使用参数属性可以简化代码:
// ✅ 推荐:使用参数属性
class User {
constructor(
public name: string,
private age: number,
protected email: string
) {}
}
// ❌ 不推荐:手动赋值
class User {
name: string;
age: number;
email: string;
constructor(name: string, age: number, email: string) {
this.name = name;
this.age = age;
this.email = email;
}
}优先使用私有字段(#)而非 private
对于真正的私有成员,使用私有字段(#)比 private 更安全:
// ✅ 推荐:使用私有字段
class Counter {
#count: number = 0; // 真正的私有,编译后仍然私有
increment() {
this.#count++;
}
}
// ❌ 不推荐:使用 private(编译后可能被访问)
class Counter {
private count: number = 0; // 编译后可能被访问
increment() {
this.count++;
}
}泛型最佳实践
使用有意义的泛型参数名
使用描述性的泛型参数名,而不是单个字母:
// ✅ 推荐:使用描述性名称
function getProperty<TObj, TKey extends keyof TObj>(
obj: TObj,
key: TKey
): TObj[TKey] {
return obj[key];
}
// ❌ 不推荐:使用单字母
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}提示
对于简单的泛型函数,使用 T、U、V 等单字母是可以接受的。但对于复杂的泛型,使用描述性名称更好。
使用泛型约束而非 any
使用泛型约束来限制类型参数,而不是使用 any:
// ✅ 推荐:使用泛型约束
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
// ❌ 不推荐:使用 any
function getLength(item: any): number {
return item.length;
}模块和导入最佳实践
使用命名导出而非默认导出
命名导出提供更好的重构支持和自动补全:
// ✅ 推荐:使用命名导出
export function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
export interface User {
name: string;
age: number;
}
// 导入
import { calculateTotal, User } from './utils';
// ❌ 不推荐:使用默认导出
export default function calculateTotal(items: Item[]): number {
// ...
}
// 导入时名称可能不一致
import calc from './utils';
import calculateTotal from './utils'; // 名称可以随意更改使用路径别名简化导入
使用路径别名可以让导入路径更清晰:
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@utils/*": ["./src/utils/*"]
}
}
}
// ✅ 推荐:使用路径别名
import { formatDate } from '@utils/date';
import { User } from '@/types';
// ❌ 不推荐:使用相对路径
import { formatDate } from '../../../utils/date';
import { User } from '../../types';类型守卫和类型断言最佳实践
优先使用类型守卫而非类型断言
类型守卫更安全,可以在运行时检查类型:
// ✅ 推荐:使用类型守卫
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function processValue(value: unknown) {
if (isString(value)) {
// TypeScript 知道 value 是 string
console.log(value.toUpperCase());
}
}
// ❌ 不推荐:使用类型断言
function processValue(value: unknown) {
const str = value as string; // 不安全,可能出错
console.log(str.toUpperCase());
}使用 satisfies 操作符(TypeScript 4.9+)
使用 satisfies 操作符可以在保持类型推断的同时进行类型检查:
// ✅ 推荐:使用 satisfies
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
} satisfies Config;
// TypeScript 会检查 config 是否符合 Config 类型
// 同时保持 config 的类型推断(而不是 Config 类型)
// ❌ 不推荐:使用类型注解
const config: Config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// config 的类型是 Config,失去了字面量类型的精确性错误处理最佳实践
使用 Result 类型处理错误
使用 Result 类型可以更好地处理错误,避免抛出异常:
// ✅ 推荐:使用 Result 类型
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return { success: false, error: new Error('Division by zero') };
}
return { success: true, data: a / b };
}
// 使用
const result = divide(10, 2);
if (result.success) {
console.log(result.data); // TypeScript 知道这是 number
} else {
console.error(result.error);
}
// ❌ 不推荐:抛出异常
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}性能最佳实践
避免深度嵌套的类型
深度嵌套的类型会影响编译性能,使用类型别名来简化:
// ❌ 不推荐:深度嵌套
type ComplexType = {
user: {
profile: {
settings: {
theme: {
colors: {
primary: string;
secondary: string;
};
};
};
};
};
};
// ✅ 推荐:使用类型别名分解
type ThemeColors = {
primary: string;
secondary: string;
};
type Theme = {
colors: ThemeColors;
};
type Settings = {
theme: Theme;
};
type Profile = {
settings: Settings;
};
type ComplexType = {
user: {
profile: Profile;
};
};使用 const 断言提高类型推断
使用 as const 可以让 TypeScript 推断出更精确的字面量类型:
// ✅ 推荐:使用 const 断言
const colors = ['red', 'green', 'blue'] as const;
// 类型:readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"
// ❌ 不推荐:不使用 const 断言
const colors = ['red', 'green', 'blue'];
// 类型:string[]代码组织最佳实践
将类型定义集中管理
将类型定义放在单独的文件中,便于管理和复用:
// types/user.ts
export interface User {
id: string;
name: string;
email: string;
}
export type UserStatus = 'active' | 'inactive' | 'suspended';
// types/api.ts
export interface ApiResponse<T> {
data: T;
status: number;
message: string;
}使用命名空间组织相关类型
对于相关的类型,使用命名空间进行组织:
// ✅ 推荐:使用命名空间
namespace UserTypes {
export interface User {
id: string;
name: string;
}
export interface UserProfile extends User {
bio: string;
avatar: string;
}
}
// 使用
const user: UserTypes.User = {
id: '1',
name: 'John'
};工具类型最佳实践
创建自定义工具类型
创建自定义工具类型可以提高代码复用性:
// ✅ 推荐:创建自定义工具类型
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// 使用:使某些属性可选
interface User {
id: string;
name: string;
email: string;
age: number;
}
type CreateUserInput = Optional<User, 'id' | 'age'>;
// { name: string; email: string; id?: string; age?: number; }注意事项
注意
- 不要过度使用类型:不是所有地方都需要明确的类型注解,让 TypeScript 的类型推断发挥作用。
- 保持类型简单:复杂的类型虽然强大,但会影响可读性和编译性能。
- 定期重构类型:随着项目发展,及时重构和改进类型定义。
- 使用严格模式:启用 TypeScript 的严格模式可以捕获更多潜在错误。
提示
最佳实践不是一成不变的,应该根据项目需求和团队约定进行调整。重要的是保持代码的一致性和可维护性。