TypeScript 中高频出现的类型结构与用法
目录
- 1.类型推断
- 2.利用交叉类型和联合类型
- 3.类型别名和接口
- 4.keyof
- 5.映射类型
- 6.泛型的使用
- 7.条件类型
- 8.infer 关键字
- 9.类型守卫
- 10.非空断言操作符与可选链
- 11.声明文件(.d.ts)
- 12.模块化类型导入
- 13.类型化事件
- 14.装饰器
- 🔹 15. const 断言(const Assertion)
- ✅总结
TypeScript 已成为现代前端开发的标配语言。它不仅帮助我们提前发现潜在错误,还能提升代码可读性、可维护性和团队协作效率。
实际开发中,仅标注基础类型(如 string、number)无法充分发挥其类型检查能力。遇到复杂类型就放弃,最后还是靠运行时调试找问题。
你有没有遇到过这些情况?
- 接口返回的数据结构复杂,手动写类型太麻烦
- 函数传对象,但不知道哪些字段可选、哪些必填
- 想复用类型,但复制粘贴了一堆重复 interface
- 用 any 一时爽,后期维护火葬场
其实这些问题,TypeScript 早就提供了工具:keyof、Pick、Partial、infer、as const……不是 TS 不好用,是你没用对。
✅本文将梳理一些 实用技巧,帮助你深入理解核心类型模式,写出更安全、更健壮、更具可扩展性的代码。
序号 | 主题 | 说明 |
---|---|---|
1 | 类型推断(Type Inference) | TS 自动识别类型,减少手动标注,提升开发效率 |
2 | 联合类型 & 交叉类型(Union & Intersection Types) | 构建灵活类型的基石,` |
3 | 类型别名 vs 接口(Type Alias vs Interface) | type 适合定义复杂类型别名,interface 更适合描述对象结构并支持声明合并 |
4 | keyof | 获取对象类型所有键的联合类型,常用于泛型约束和属性安全访问 |
5 | 映射类型(Mapped Types) | 基于已有类型生成新类型,如 Partial<T> 、Pick<T, K> 、Readonly<T> 、Record<K, T> |
6 | 泛型(Generics) | 实现可复用的函数、类和组件的核心机制,提升代码灵活性与类型安全 |
7 | 条件类型(Conditional Types) | 根据类型关系进行判断选择,语法为 T extends U ? X : Y ,用于构建高级类型逻辑 |
8 | infer 关键字 | 在条件类型中“推断”出子类型,常用于提取数组元素、函数返回值、Promise 解包等场景 |
9 | 类型守卫(Type Guards) | 在运行时缩小联合类型范围,包括 typeof 、instanceof 、in 和自定义守卫函数 |
10 | 非空断言(!)与可选链(?.) | ! 告诉 TS 值不为 null/undefined;?. 安全访问嵌套属性,避免运行时错误 |
11 | 声明文件(.d.ts) | 为 JavaScript 库或全局变量提供类型定义,提升第三方代码的类型安全性 |
12 | 模块化类型导入(import type) | 使用 import type 导入仅用于类型的模块,避免运行时引入,优化打包体积 |
13 | 类型化事件(Typed Event) | 为事件系统添加参数类型约束,实现类型安全的发布-订阅模式 |
14 | 装饰器(Decorators) | 使用 @decorator 修饰类、方法、属性,常见于 Angular、NestJS 等框架中 |
15 | const 断言(as const ) | 将字面量对象或数组标记为完全只读和最小化类型,保留字面量类型(如 'dark' 而非 string ),适用于配置、常量和模拟枚举 |
1.类型推断
类型推断(Type Inference)是 TypeScript 的一个强大的特性。它允许编译器根据上下文自动推断出变量的类型,从而减少手动输入类型的工作量,同时也提高了代码的可维护性和可读性。
✅优势:减少冗余代码,提升开发效率, 示例:
let num = 5; // 推断为 number
let str = "hello"; // 推断为 string
let bool = true; // 推断为 booleanfunction add(a: number, b: number) {return a + b; // 推断返回值为 number
}let result = add(num, 10);
2.利用交叉类型和联合类型
交叉类型(Intersection Types)允许将多个类型合并为一个类型,新类型将具有所有类型的特性。我们可以使用符号 & 运算符将两个或多个类型组合成一个交叉类型。
联合类型(Union Types)表示一个值可以有多种类型之一。我们可以使用符号 | 运算符将两个或多个类型组合成一个联合类型。
✅交叉类型(&)
将多个类型合并为一个新类型,拥有所有成员。
interface Dog { walk(): void }
interface Cat { meow(): void }type Pet = Dog & Cat;const myPet: Pet = {walk() { console.log('walking') },meow() { console.log('meowing') }
}
✅ 应用场景:联合类型用于多态输入,交叉类型用于 mixin 或组合对象。
✅联合类型(|)
联合类型的用法就是使用 | 运算符将多个类型组合在一起,表示一个值可以是多种类型之一。
interface Square {side: number;
}interface Circle {radius: number;
}function calculateArea(shape: Square | Circle) {if ('side' in shape) {return shape.side ** 2;} else {return Math.PI * shape.radius ** 2;}
}
type Color = 'red' | 'green' | 'blue';
type ID = string | number;function printId(id: ID) {console.log(id.toUpperCase()); // ❌ error: number 没有 toUpperCase
}
3.类型别名和接口
1.类型别名
类型别名(Type Aliases)是一种给一个已经存在的类型起一个新的名字的方式。通过 type 关键字可以定义一个类型别名,支持原始类型、联合、交叉等。
type MyString = string;
type MyNumber = number;type Person = {name: string;
age: number;
};
type Person = {name: string;age: number;
};type Status = 'active' | 'inactive';
类型别名可以很方便地给复杂的类型定义一个简洁的名称,从而提高代码可读性,并且还可以使用联合类型、交叉类型等高级类型
type Color = 'red' | 'green' | 'blue';
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; length: number };function draw(shape: Shape, color: Color) {// ...
}
2.接口
接口(Interfaces)是一种描述对象结构的方式,在 TypeScript 中通过 interface 关键字来定义。接口可以包含属性、方法和索引签名等
interface Person {name: string;age: number;sayHello: () => void;
}let person: Person = {name: 'Alice',age: 30,sayHello() {console.log(`Hello, my name is ${this.name}.`);},
};
接口在描述对象结构时非常有用,它可以提供更好的代码组织性和可读性,并且也可以在一些特定场景下提供更好的类型安全性。另外需要注意的是,接口只能描述对象的形状,不能描述具体的实现方式。如果需要描述具体的实现方式,可以使用类或函数类型。
4.keyof
keyof 是 TypeScript 中的一个关键字,用于获取对象类型的所有键的联合类型。它可以帮助我们在编写泛型函数或操作对象属性时,提供更好的类型安全性,常用于泛型约束。
✅ 作用:实现类型安全的属性访问。
interface Person {name: string;age: number;gender: 'male' | 'female';
}function getProperty<T, K extends keyof T>(obj: T, key: K) {return obj[key];
}let person: Person = { name: 'Alice', age: 30, gender: 'female' };
let name = getProperty(person, 'name');
let age = getProperty(person, 'age');
let gender = getProperty(person, 'gender');
5.映射类型
TypeScript 中的映射类型(Mapped Types)是一种非常强大的类型操作符,它可以根据一个已有的对象类型,生成一个新的对象类型。映射类型可以帮助我们进行一些常见的类型转换和操作,如将所有属性变成可选属性、添加或删除属性、修改属性类型等等。
TypeScript 中的映射类型有以下四种:
- Partial:将类型 T 中所有属性变为可选属性。
interface Person {name: string;age: number;gender: 'male' | 'female';
}type PartialPerson = Partial<Person>;// 等价于
// interface PartialPerson {
// name?: string;
// age?: number;
// gender?: 'male' | 'female';
// }
- Readonly:将类型 T 中所有属性变为只读属性。
interface Person {name: string;age: number;gender: 'male' | 'female';
}type ReadonlyPerson = Readonly<Person>;// 等价于
// interface ReadonlyPerson {
// readonly name: string;
// readonly age: number;
// readonly gender: 'male' | 'female';
// }
- Record<K, T>:创建一个新的对象类型,其属性名类型为 K,属性值类型为 T
type Dictionary<T> = Record<string, T>;let dict: Dictionary<number> = {foo: 123,bar: 456,
};
- Pick<T, K>:从类型 T 中选择指定的属性 K,并返回一个新的对象类型。
interface Person {name: string;age: number;gender: 'male' | 'female';
}type PersonNameAndAge = Pick<Person, 'name' | 'age'>;// 等价于
// interface PersonNameAndAge {
// name: string;
// age: number;
// }
还有一种映射类型叫做 Keyof,它用于获取一个对象类型中所有属性名组成的联合类型。这个类型在前面的问题中已经讲到过了,这里就不再赘述。
6.泛型的使用
泛型可以让我们编写更具灵活性、可重用性和类型安全性的代码。在 TypeScript 中,泛型通常使用类型参数来定义一个通用的类型或函数,并在使用时指定具体的类型。
我们想编写一个函数来反转任意数组,假设我们不使用泛型,代码可能会是这样:
function reverseStrings(items: string[]): string[] {return items.reverse();
}function reverseNumbers(items: number[]): number[] {return items.reverse();
}
但是这种方法显然不够优雅,因为我们需要分别编写两个函数来处理 string 和 number 类型的数组,并且当我们需要处理其他类型的数组时,我们必须再次编写新的函数。
使用泛型,我们可以很容易地创建一个通用的函数来处理任何类型的数组:
function reverse<T>(items: T[]): T[] {return items.reverse();
}const words = ['hello', 'world'];
const reversedWords = reverse<string>(words);
console.log(reversedWords); // ['world', 'hello']const numbers = [1, 2, 3];
const reversedNumbers = reverse<number>(numbers);
console.log(reversedNumbers); // [3, 2, 1]
✅ 核心思想:用类型参数 T 实现“一次编写,多处使用”。
7.条件类型
条件类型(Conditional Types)允许我们根据类型之间的关系来选择不同的类型。它的语法类似于三元运算符:T extends U ? X : Y,表示如果 T 可以赋值给 U,则结果为 X,否则为 Y。
这在处理复杂类型逻辑时非常有用,比如过滤类型、提取结构等。
✅ 使用场景
// 判断是否为字符串类型
type IsString<T> = T extends string ? true : false;type A = IsString<'hello'>; // true
type B = IsString<123>; // false// 过滤联合类型中的某些成员
type FilterString<T> = T extends string ? T : never;
type OnlyStrings = FilterString<'a' | 'b' | 1 | 2>; // 'a' | 'b'
✅ 内置工具类型
TypeScript 内置了很多基于条件类型的工具:
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type MyNonNullable<T> = T extends null | undefined ? never : T;
这些是 Exclude<T, U>、Extract<T, U>、NonNullable 的底层实现原理。
8.infer 关键字
infer 是 “infer”(推断)的缩写, 在条件类型中推断类型,用于在 extends 子句中声明一个待推断的类型变量。它常与条件类型配合使用,用来“提取”复杂类型中的子类型。
✅ 使用场景
// 提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<number[]>; // number
type B = ElementType<string>; // string// 提取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
type Fn = () => string;
type R = ReturnType<Fn>; // string// 提取 Promise 解包后的类型
type Unpacked<T> = T extends Promise<infer U> ? U : T;
type Data = Unpacked<Promise<string>>; // string
✅ 实际应用:当你调用一个返回 Promise 的 API 时,可以用 infer 自动解析出 T,避免手动声明。
9.类型守卫
类型守卫(Type Guards)是 TypeScript 中用来检测类型的一种机制,它可以帮助开发者在运行时检测某个变量的类型,并在不同的条件下提供不同的类型声明。
在 TypeScript 中,有四种常见的类型守卫方式:
- typeof 类型守卫
function foo(x: number | string) {if (typeof x === 'number') {// x is number} else {// x is string}
}
- instanceof 类型守卫
class MyClass {}function foo(x: any) {if (x instanceof MyClass) {// x is an instance of MyClass}
}
- 自定义类型守卫函数
interface A { a: number }
interface B { b: number }function isA(x: any): x is A {return typeof x.a === 'number';
}function foo(x: A | B) {if (isA(x)) {// x is an instance of A} else {// x is an instance of B}
}
- in 操作符类型守卫
interface A { a: number }
interface B { b: number }function foo(x: A | B) {if ('a' in x) {// x is an instance of A} else {// x is an instance of B}
}
10.非空断言操作符与可选链
非空断言操作符(!)与可选链(?.)这两个特性极大提升了处理可能为 null 或 undefined 值的安全性和简洁性。
✅ 非空断言操作符 !
告诉 TypeScript 编译器:“我确定这个值不是 null 或 undefined”。
const el = document.getElementById('app')!;
el.innerHTML = 'Hello World'; // 不会报错,即使 getElementById 返回可能为 null
⚠️ 注意:使用 ! 要谨慎,确保运行时确实存在,否则会导致 JS 运行时错误。
✅ 可选链操作符 ?.
安全地访问嵌套对象属性,避免 Cannot read property ‘x’ of undefined 错误。
interface User {profile?: {address?: {city?: string;};};
}const user: User = {};// 安全访问
const city = user.profile?.address?.city; // string | undefined// 也可用于函数调用
user.logout?.(); // 如果 logout 存在就调用
✅ 推荐组合使用
结合空值合并 ??,实现默认值 fallback。
const name = users[0]?.name ?? 'Unknown';
11.声明文件(.d.ts)
声明文件(Declaration File)是一种特殊的类型文件,用来描述外部 JavaScript 库、模块或对象的类型,以便在 TypeScript 代码中正确引用和使用它们。
TypeScript 编译器可以根据 JavaScript 库的源代码推断出其类型信息,但某些 JavaScript 库并没有提供类型定义文件,或者类型定义文件不完整或不准确,这时我们需要手动编写声明文件。声明文件的扩展名为 .d.ts,可以与 TypeScript 文件一起放置在项目目录中。声明文件的编写方式有以下几种:
- 定义全局变量和函数
如果我们需要在 TypeScript 代码中调用浏览器原生 API 或其他 JavaScript 库中的全局变量和函数,就需要手动编写声明文件来告诉 TypeScript 对应变量和函数的类型。例如:
// global.d.ts
declare const $: (selector: string) => any;$('#my-element').addClass('active');
- 扩展已有类型
有时候我们需要扩展已有的类型定义,以适应自己的需求,这时可以使用 interface、namespace 等关键字来定义和扩展类型。例如:
interface String {reverse(): string;
}const str = 'Hello, world!';
console.log(str.reverse()); // "!dlrow ,olleH"
- 模块声明
如果我们要使用一个已有的 JavaScript 模块,但模块本身没有提供类型定义文件,或者类型定义文件不完整或不准确,这时我们需要手动编写声明文件来告诉 TypeScript 模块的类型信息。例如:
declare module 'my-lib' {export function greet(name: string): string;
}
12.模块化类型导入
在大型项目中,合理组织类型文件至关重要。TypeScript 支持 ES Module 和 namespace 两种方式,推荐使用模块化方式。
✅ 模块化类型导出(推荐)
// types/user.ts
export interface User {id: number;name: string;email?: string;
}export type Role = 'admin' | 'user' | 'guest';
// main.ts
import type { User, Role } from './types/user'; // 使用 import type 只导入类型,不生成 JS 代码const currentUser: User = { id: 1, name: 'Alice' };
✅ import type 是编译期导入,编译后不生成代码,提升性能;确保类型不会出现在运行时代码中,提升性能;支持类型拆分,便于维护。
13.类型化事件
类型化事件(Typed Event)是一种可以指定事件处理函数接收参数类型、返回值类型的事件机制。通过使用类型化事件,我们可以在编译时对事件处理函数的类型进行检查,以避免运行时因类型不匹配而导致的错误。
✅示例,如何定义和使用类型化事件:
interface EventHandler<T> {(args: T): void;
}class TypedEvent<T> {private handlers: EventHandler<T>[] = [];public addHandler(handler: EventHandler<T>) {this.handlers.push(handler);}public removeHandler(handler: EventHandler<T>) {const index = this.handlers.indexOf(handler);if (index >= 0) {this.handlers.splice(index, 1);}}public raise(args: T) {for (const handler of this.handlers) {handler(args);}}
}// 定义一个事件参数类型
interface LoginEventArgs {message: string;
}// 创建一个类型化事件实例
const onLogin = new TypedEvent<LoginEventArgs >();// 添加一个事件处理函数
myEvent.addHandler((args: MyEventArgs) => {console.log(args.message); // 类型安全
});// 触发事件
onLogin .raise({ message: 'Hello, world!' });
14.装饰器
装饰器是一种特殊的语法,它可以用来修饰类、方法、属性以及参数等元素,从而达到一些特定的目的。在 TypeScript 中,我们可以使用 @ 符号来声明一个装饰器
function log(target: any, key: string, descriptor: PropertyDescriptor) {const original = descriptor.value;descriptor.value = function(...args: any[]) {console.log(`Calling ${key} with`, args);return original.apply(this, args);};return descriptor;
}class UserService {@loglogin(username: string) {console.log(`User ${username} logged in`);}
}new UserService().login('Alice');
// 输出:
// Calling login with ["Alice"]
// User Alice logged in
✅ TypeScript 中的装饰器可以用于很多场景,例如实现依赖注入、自动绑定事件、路由映射等等。常见的装饰器包括 @Injectable、@Component、@ViewChild、@RouterConfig 等等。
🔹 15. const 断言(const Assertion)
const 断言是 TypeScript 中一种强大的字面量类型控制方式,它能帮助我们最小化类型推断的“宽松性”,让字面量对象、数组等保持最具体的类型,避免不必要的类型扩展。
它使用 as const 语法,告诉 TypeScript:“请把这个值当作完全不可变的字面量来处理”。
✅ 为什么需要 const 断言?
默认情况下,TypeScript 会对对象和数组进行“宽松推断”,可能导致类型不够精确。
示例:没有 const 断言的问题
const config = {mode: 'dark',timeout: 3000,enabled: true,tags: ['react', 'ts']
};// config 的类型实际上是:
// {
// mode: string;
// timeout: number;
// enabled: boolean;
// tags: string[];
// }// 如果你希望 mode 只能是 'dark' | 'light',这里已经丢失了字面量信息!
此时 config.mode 的类型是 string,而不是 ‘dark’,这会导致类型检查失效。
✅ 使用 const 断言解决
const config = {mode: 'dark',timeout: 3000,enabled: true,tags: ['react', 'ts']
} as const;// 现在 config 的类型是:
// readonly {
// readonly mode: "dark";
// readonly timeout: 3000;
// readonly enabled: true;
// readonly tags: readonly ["react", "ts"];
// }
mode 的类型是字面量 ‘dark’(而不是 string)
tags 是 readonly [“react”, “ts”],长度和内容都被固定
所有属性和数组都变为 readonly,防止意外修改
✅ 实际应用场景
- 定义常量配置(推荐)
const ENV = {API_URL: 'https://api.example.com',VERSION: '1.0.0',FEATURES: {darkMode: true,analytics: false}
} as const;// 后续使用时,TypeScript 知道 ENV.API_URL 就是字符串字面量
// 可用于联合类型匹配、环境判断等
- 联合类型中的字面量集合
const COLORS = ['red', 'green', 'blue'] as const;
type Color = typeof COLORS[number]; // 'red' | 'green' | 'blue'function paint(color: Color) {console.log(`Painting with ${color}`);
}paint('red'); // ✅ OK
paint('pink'); // ❌ Error: not in union
- 模拟枚举(无编译输出)
const Direction = {Up: 'UP',Down: 'DOWN',Left: 'LEFT',Right: 'RIGHT'
} as const;type Direction = typeof Direction[keyof typeof Direction];
// 等价于: 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
✅ 优势:没有编译后的 JS 输出(不像 enum),更轻量。
as const 是“类型最小化”的利器,它让字面量保持“原形”,避免 TypeScript 过度放宽类型,是构建精确类型系统的关键一环。
✅总结
📌 TypeScript 的目标不是“写更多类型”,而是“写更少 bug”。
TypeScript 不只是一个“加类型”的工具,它是一套完整的类型系统,能帮助我们写出更安全、可维护、可复用的代码。掌握这些核心技巧,不仅能提升开发效率,还能在团队协作中减少 bug、提升代码质量。建议从基础开始,逐步深入泛型、条件类型等高级特性,最终在项目中形成自己的“类型设计模式”。