跳到主要内容

TypeScript 高级特性

TypeScript 提供了许多高级类型和特性,掌握这些可以帮助我们写出更加健壮和类型安全的代码。

高级类型

1. 交叉类型(Intersection Types)

使用 & 运算符将多个类型合并为一个类型:

interface BusinessPartner {
name: string;
credit: number;
}

interface Identity {
id: number;
email: string;
}

type Employee = BusinessPartner & Identity;

// 使用示例
let emp: Employee = {
name: "John",
credit: 100,
id: 1234,
email: "john@example.com"
};

2. 联合类型(Union Types)

使用 | 运算符表示一个值可以是几种类型之一:

type StringOrNumber = string | number;
type Status = "success" | "error" | "pending";

function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}

3. 类型守卫(Type Guards)

帮助TypeScript推断类型的表达式:

// typeof 类型守卫
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error("Expected string or number");
}

// instanceof 类型守卫
class Bird {
fly() {
console.log("flying...");
}
}

class Fish {
swim() {
console.log("swimming...");
}
}

function move(pet: Bird | Fish) {
if (pet instanceof Bird) {
pet.fly();
} else {
pet.swim();
}
}

4. 映射类型(Mapped Types)

从现有类型创建新类型:

// Readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

// Partial
type Partial<T> = {
[P in keyof T]?: T[P];
};

// Pick
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

// Record
type Record<K extends keyof any, T> = {
[P in K]: T;
};

5. 条件类型(Conditional Types)

根据条件选择类型:

type NonNullable<T> = T extends null | undefined ? never : T;

// 示例
type EmailAddress = string | null | undefined;
type NonNullableEmail = NonNullable<EmailAddress>; // string

// 条件类型与泛型结合
type ArrayType<T> = T extends any[] ? T[number] : T;
type StringArray = ArrayType<string[]>; // string
type NumberValue = ArrayType<number>; // number

1. 装饰器(Decorators)

装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问符、属性或参数上。装饰器使用 @expression 形式,expression 求值后必须为一个函数。

1.1 类装饰器

// 类装饰器
function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}

@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}

console.log(new Greeter("world"));

1.2 方法装饰器

// 方法装饰器
function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 保存原始方法
const originalMethod = descriptor.value;

// 修改方法行为
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned:`, result);
return result;
};
}

class Example {
@methodDecorator
greet(name: string) {
return `Hello, ${name}!`;
}
}

1.3 属性装饰器

// 属性装饰器
function propertyDecorator(target: any, propertyKey: string) {
// 属性getter
let value = target[propertyKey];

// 属性getter/setter
Object.defineProperty(target, propertyKey, {
get: () => value,
set: (newValue: any) => {
console.log(`Setting ${propertyKey} to:`, newValue);
value = newValue;
},
});
}

class User {
@propertyDecorator
name: string = "default";
}

1.4 参数装饰器

// 参数装饰器
function parameterDecorator(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter decorator: ${propertyKey}, index: ${parameterIndex}`);
}

class Service {
login(@parameterDecorator username: string, @parameterDecorator password: string) {
// 方法实现
}
}

1.5 装饰器工厂

// 装饰器工厂
function log(prefix: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;

descriptor.value = function(...args: any[]) {
console.log(`${prefix} ${propertyKey}`);
return originalMethod.apply(this, args);
};
};
}

class Calculator {
@log("Calling method:")
add(x: number, y: number) {
return x + y;
}
}

2. 模块和命名空间

2.1 模块

TypeScript 中的模块可以帮助我们更好地组织代码,实现代码复用和封装。

// math.ts
export function add(x: number, y: number): number {
return x + y;
}

export function subtract(x: number, y: number): number {
return x - y;
}

// 默认导出
export default class Calculator {
add(x: number, y: number): number {
return x + y;
}
}

// main.ts
import Calculator, { add, subtract } from './math';

const calc = new Calculator();
console.log(calc.add(1, 2)); // 3
console.log(add(3, 4)); // 7
console.log(subtract(10, 5)); // 5

2.2 命名空间

命名空间可以用来避免命名冲突,将相关的代码组织在一起。

// 定义命名空间
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}

export class LettersOnlyValidator implements StringValidator {
isValid(s: string): boolean {
return /^[A-Za-z]+$/.test(s);
}
}

export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return /^\d{5}(-\d{4})?$/.test(s);
}
}
}

// 使用命名空间
let validators: { [s: string]: Validation.StringValidator } = {};
validators["Letters"] = new Validation.LettersOnlyValidator();
validators["ZIP"] = new Validation.ZipCodeValidator();

3. 高级类型技巧

3.1 类型保护

// 类型谓词
function isString(value: any): value is string {
return typeof value === "string";
}

// instanceof 类型保护
class Bird {
fly() {}
layEggs() {}
}

class Fish {
swim() {}
layEggs() {}
}

function getSmallPet(): Fish | Bird {
return Math.random() > 0.5 ? new Bird() : new Fish();
}

let pet = getSmallPet();
if (pet instanceof Bird) {
pet.fly();
}

3.2 可辨识联合类型

interface Square {
kind: "square";
size: number;
}

interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}

interface Circle {
kind: "circle";
radius: number;
}

type Shape = Square | Rectangle | Circle;

function area(s: Shape): number {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.width * s.height;
case "circle": return Math.PI * s.radius ** 2;
}
}

3.3 映射类型进阶

// 条件类型中的映射
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];

interface User {
name: string;
age: number;
updateProfile: () => void;
}

type FunctionProps = FunctionPropertyNames<User>; // "updateProfile"

// 映射类型修饰符
type Mutable<T> = {
-readonly [P in keyof T]: T[P]
};

type Required<T> = {
[P in keyof T]-?: T[P]
};

4. 声明合并

TypeScript中独特的概念,它可以将多个同名声明合并为一个定义。

// 接口合并
interface Box {
height: number;
width: number;
}

interface Box {
scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};

// 命名空间和类合并
class Album {
label: Album.AlbumLabel;
}

namespace Album {
export interface AlbumLabel {
name: string;
}
}

5. 实用工具类型

// 内置工具类型的高级用法
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
};

type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
};

// 条件类型工具
type Unpacked<T> = T extends (infer U)[]
? U
: T extends (...args: any[]) => infer U
? U
: T extends Promise<infer U>
? U
: T;

type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string

6. 最佳实践

6.1 类型安全

// 严格的空值检查
function processValue(value: string | null | undefined) {
if (value != null) { // 同时检查null和undefined
console.log(value.toUpperCase());
}
}

// 使用类型断言时的安全检查
function assertIsString(value: any): asserts value is string {
if (typeof value !== "string") {
throw new Error("Value must be a string");
}
}

6.2 性能优化

// 避免过度使用泛型
// 不好的做法
function identity<T>(value: T): T {
return value;
}

// 更好的做法
function identity(value: any): any {
return value;
}

// 合理使用类型推断
// 不必要的类型注解
const numbers: number[] = [1, 2, 3];

// 让TypeScript自动推断
const numbers = [1, 2, 3];

6.3 代码组织

// 使用barrel文件组织导出
// models/index.ts
export * from './user.model';
export * from './product.model';
export * from './order.model';

// 使用命名空间组织相关功能
namespace App {
export namespace Utils {
export function format(value: string): string {
return value.trim();
}
}

export namespace Validation {
export function validate(value: string): boolean {
return value.length > 0;
}
}
}

7. 其他常见高级用法

前面那些内容更偏类型系统本身,这一节补一些项目里更常见、也更容易直接用上的高级写法。

7.1 infer:从已有类型里提取类型

infer 一般配合条件类型使用。它的作用很直接:如果某个类型满足某种结构,就把结构里的某一部分提取出来。

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
return { id: 1, name: "Tom" };
}

type User = MyReturnType<typeof getUser>;
// { id: number; name: string }

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number

7.2 模板字面量类型

模板字面量类型适合约束固定格式的字符串,比如事件名、位置名、主题变量名。

type Direction = "top" | "bottom" | "left" | "right";
type Align = "start" | "center" | "end";

type Position = `${Direction}-${Align}`;

let p1: Position = "top-start";
let p2: Position = "left-center";
// let p3: Position = "middle-center"; // Error

type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<"click">; // "onClick"
type ChangeEvent = EventName<"change">; // "onChange"

7.3 键重映射

映射类型除了能遍历属性,还能通过 as 重新生成 key。这个技巧很适合做 getter、setter、接口映射。

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

type Getter<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

type UserGetter = Getter<User>;

// 结果:
// {
// getName: () => string;
// getAge: () => number;
// }

7.4 satisfies

satisfies 很实用。它能检查对象是否满足目标类型,同时尽量保留原始对象更精确的推断结果。

type Route = {
path: string;
needAuth: boolean;
};

const routes = {
home: { path: "/", needAuth: false },
profile: { path: "/profile", needAuth: true }
} satisfies Record<string, Route>;

// routes 需要符合 Record<string, Route>
// 但 routes.home.path 仍然能保留更具体的字面量信息

使用描述: satisfies 用来检查“这个值是否满足某个目标类型”,但不会像 as 那样粗暴地把值强行断言成那个类型。

功能为了实现什么: 它主要是为了解决配置对象场景里的两个矛盾:

  • 你希望 TypeScript 帮你校验字段结构对不对
  • 你又不希望丢掉这个对象本身更精确的推断结果

所以它特别适合路由、导航、表单 schema、埋点配置、按钮配置这类“数据驱动 UI”的地方。

7.5 断言函数(asserts)

普通类型守卫是“判断一下”,断言函数更进一步:如果函数能正常执行完,TypeScript 就认为类型已经被缩小了。

function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("value must be string");
}
}

function printUpperCase(value: unknown) {
assertIsString(value);
console.log(value.toUpperCase());
}

7.6 this 参数和链式调用

TypeScript 可以显式声明函数里的 this 类型。这个写法在工具函数、对象方法和链式 API 里都挺常见。

type User = {
name: string;
say(this: User): void;
};

const user: User = {
name: "Tom",
say() {
console.log(this.name);
}
};
class QueryBuilder {
private sql = "";

select(fields: string): this {
this.sql += `SELECT ${fields} `;
return this;
}

from(table: string): this {
this.sql += `FROM ${table}`;
return this;
}
}

7.7 as const + typeof + keyof 联动

这组组合拳在“配置驱动”的代码里特别常见:先从常量值推导类型,再把这个类型拿去约束别的地方。

const statusMap = {
success: 200,
notFound: 404,
serverError: 500
} as const;

type StatusKey = keyof typeof statusMap;
type StatusCode = typeof statusMap[StatusKey];

let key: StatusKey = "success";
let code: StatusCode = 404;

使用描述:

  • as const 会把对象里的值尽量收窄成字面量类型,同时把属性变成只读
  • typeof 用来从真实值拿到它的类型
  • keyof 再把这个类型的键提取出来

功能为了实现什么: 它主要是为了解决“配置和类型写了两份,后面容易不一致”的问题。你先写真实配置对象,再从对象反推出 key 和 value 的类型,这样配置变了,类型也会跟着变。

项目里常见用途:

  • 从菜单配置推导菜单 key
  • 从权限映射推导权限名
  • 从状态常量推导联合类型
  • 从接口定义对象推导方法名

7.8 never 做穷尽检查

可辨识联合真正好用的地方,是能在 switch 里做穷尽检查。以后如果新增一种类型,漏写分支时编译器会提醒你。

type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number };

function assertNever(value: never): never {
throw new Error(`Unhandled value: ${value}`);
}

function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
default:
return assertNever(shape);
}
}

7.9 const 泛型

这是 TS 新一些的写法,适合保留更精确的字面量类型,尤其在处理元组、路由配置、命令定义时很好用。

function makeTuple<const T extends readonly unknown[]>(value: T): T {
return value;
}

const t1 = makeTuple([1, 2, 3]);
// readonly [1, 2, 3]

const t2 = makeTuple(["get", "/users"]);
// readonly ["get", "/users"]

7.10 组合例子:从配置推导类型

这些能力放在一起,最典型的场景就是“配置写一次,类型自动跟着走”。

const api = {
getUser: { method: "GET", path: "/user/:id" },
createUser: { method: "POST", path: "/user" }
} as const;

type ApiName = keyof typeof api;
type ApiMethod = typeof api[ApiName]["method"];

function request(name: ApiName) {
return api[name];
}

const result = request("getUser");
// result.method 只能是 "GET"
// result.path 只能是 "/user/:id"

7.11 声明补充

declare module "*.svg" {
const src: string;
export default src;
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
}

这类内容通常会放在 env.d.tsglobal.d.tstypes/*.d.ts 中。

使用描述: 声明补充本质上是在告诉 TypeScript:“这个模块、这个全局对象、这个环境变量在项目里确实存在,你要按我补充的规则来理解它。”

功能为了实现什么: 它主要是为了解决两类问题:

  1. TypeScript 默认并不知道你项目里的一些运行时约定,比如 import.meta.env 里到底有哪些字段
  2. 某些静态资源、第三方模块或全局对象没有完整类型,导致项目里一导入就报错

所以声明补充常用来打通“项目真实运行环境”和“TypeScript 类型系统”之间的那层缝隙。

项目里常见用途:

  • VITE_*process.env 补环境变量声明
  • svgpngmd 这类资源导入补模块声明
  • 给没有类型的第三方库补最小可用声明
  • 给全局对象或框架扩展字段补声明

8. 使用建议

高级类型很强,但不是越复杂越好。

  • 普通接口能说明白的事,就别堆太多条件类型
  • 团队里没人看得懂的类型技巧,后期维护会很痛苦
  • infer、模板字面量类型、satisfies 这几个通常投入产出比很高
  • 复杂工具类型最好单独放到 types.ts 里,不要散落在业务文件中

一句话:让类型帮你兜底,不要让类型本身变成负担。