mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1702 字
9 分钟
类 型 擦 除 一 定 要 懂

背景#

TypeScript 作为 JavaScript 的超集语言,在编译时提供了强大的类型检查能力。但是,当 TypeScript 代码编译为 JavaScript 后,所有的类型信息都会消失——这就是”类型擦除”。理解类型擦除的机制对于正确编写 TypeScript 代码至关重要。

什么是类型擦除#

类型擦除(Type Erasure)是 TypeScript 编译过程中的核心特性。简单来说,TypeScript 的类型系统只存在于编译时,当代码编译为 JavaScript 后,所有的类型注解、接口定义、类型别名等都会被完全移除。

基础示例:

// TypeScript 源码
interface User {
name: string;
age: number;
}
const user: User = {
name: "L1ngg",
age: 20,
};
function greet(user: User): string {
return `Hello, ${user.name}!`;
}

编译后的 JavaScript:

// 所有类型信息都消失了
const user = {
name: "L1ngg",
age: 20,
};
function greet(user) {
return `Hello, ${user.name}!`;
}

类型擦除的工作原理#

理解哪些内容会被擦除、哪些会保留,对于编写正确的代码至关重要。

会被完全擦除的内容#

1. 接口(Interface)

// TS 源码
interface User {
name: string;
age: number;
}
const user: User = { name: "L1ngg", age: 20 };
// 编译后的 JS - interface 完全消失
const user = { name: "L1ngg", age: 20 };

2. 类型别名(Type Alias)

// TS 源码
type ID = string | number;
const userId: ID = "123";
// 编译后的 JS - type 完全消失
const userId = "123";

3. 泛型参数(Generics)

// TS 源码
function getFirst<T>(arr: T[]): T {
return arr[0];
}
// 编译后的 JS - 泛型参数 T 完全消失
function getFirst(arr) {
return arr[0];
}

4. 类型注解(Type Annotations)

// TS 源码
const name: string = "L1ngg";
const age: number = 20;
// 编译后的 JS - 类型注解完全消失
const name = "L1ngg";
const age = 20;

不会被擦除的内容#

1. 类(Class)

类是 TypeScript 中既是类型也是值的特殊存在。类的定义会保留在编译后的 JavaScript 中。

// TS 源码
class User {
constructor(
public name: string,
public age: number,
) {}
greet(): string {
return `Hello, I'm ${this.name}`;
}
}
// 编译后的 JS - 类会保留(转换为 ES5 或保持 ES6 语法)
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}

2. 枚举(Enum)- 特殊情况

枚举的处理比较特殊,普通枚举会保留,但 const 枚举会被擦除。

// 普通枚举 - 会保留在 JS 中
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
}
// 编译后的 JS - 枚举会转换为对象
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
})(Status || (Status = {}));
// const 枚举 - 会被完全擦除
const enum Color {
Red = "#FF0000",
Blue = "#0000FF",
}
const myColor = Color.Red;
// 编译后的 JS - const enum 被内联,枚举定义消失
const myColor = "#FF0000";

类型擦除的实际影响#

理解类型擦除很重要,因为它直接影响代码的运行时行为。

1. 不能在运行时检查接口或类型别名#

由于接口和类型别名在编译后完全消失,无法使用 instanceoftypeof 来检查它们。

interface User {
name: string;
age: number;
}
const obj = { name: "L1ngg", age: 20 };
// ❌ 错误:接口在运行时不存在
if (obj instanceof User) {
console.log("Is a User");
}
// ✅ 正确:使用类型守卫函数
function isUser(obj: any): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
typeof obj.name === "string" &&
typeof obj.age === "number"
);
}
if (isUser(obj)) {
console.log("Is a User");
}

2. 类可以在运行时使用#

因为类会保留在 JavaScript 中,所以可以用于运行时检查。

class Admin {
role = "admin";
constructor(public name: string) {}
}
const user = new Admin("L1ngg");
// ✅ 正确:类在运行时存在
if (user instanceof Admin) {
console.log("Is an Admin");
}

3. 泛型信息在运行时不可用#

泛型参数在编译后会被擦除,无法在运行时获取泛型的具体类型。

function processArray<T>(arr: T[]): void {
// ❌ 无法在运行时知道 T 的具体类型
// typeof T 是不可能的
console.log(arr);
}
// 如果需要运行时类型信息,需要显式传递
function processArrayWithType<T>(arr: T[], type: new () => T): void {
console.log(`Processing array of ${type.name}`);
}

import type 与类型擦除#

理解类型擦除后,我们可以更好地利用 import type 来优化代码。

为什么需要 import type#

当我们只需要导入类型信息(用于类型标注),而不需要运行时的值时,使用 import type 可以明确告诉编译器和打包工具:这个导入会被完全擦除。

types.ts
export interface UserProfile {
id: string;
name: string;
}
export const API_URL = "https://api.example.com";
// user.ts
import type { UserProfile } from "./types"; // 只导入类型
import { API_URL } from "./types"; // 导入值
const user: UserProfile = {
id: "1",
name: "L1ngg",
};
fetch(`${API_URL}/users`);

编译后:

user.js
import { API_URL } from "./types"; // UserProfile 的导入消失了
const user = {
id: "1",
name: "L1ngg",
};
fetch(`${API_URL}/users`);

import type 的好处#

  1. 打包优化:避免将仅用于类型标注的模块打包进最终代码,减小打包体积
  2. 明确意图:让其他开发者一眼看出这是纯类型导入
  3. 避免副作用:某些模块在导入时会执行代码,使用 import type 可以避免这些副作用
  4. 解决循环依赖:在某些循环依赖场景下,import type 可以打破循环引用

实际应用场景#

1. 使用类型守卫进行运行时检查#

由于类型信息会被擦除,我们需要使用类型守卫函数来进行运行时类型检查。

interface ApiResponse {
success: boolean;
data: any;
}
function isApiResponse(obj: any): obj is ApiResponse {
return (
typeof obj === "object" &&
obj !== null &&
typeof obj.success === "boolean" &&
"data" in obj
);
}
// 使用类型守卫
const response = await fetch("/api/data").then((r) => r.json());
if (isApiResponse(response)) {
// TypeScript 知道这里 response 是 ApiResponse 类型
console.log(response.data);
}

2. 使用类进行运行时类型检查#

当需要运行时类型检查时,考虑使用类而不是接口。

// 使用类而不是接口
class User {
constructor(
public name: string,
public age: number,
) {}
}
const obj = new User("L1ngg", 20);
// ✅ 可以在运行时检查
if (obj instanceof User) {
console.log("Is a User instance");
}

3. 优化打包体积#

使用 import type 可以避免不必要的模块被打包。

types.ts
export interface Config {
apiUrl: string;
}
export const DEFAULT_CONFIG: Config = {
apiUrl: "https://api.example.com",
};
// app.ts
import type { Config } from "./types"; // 只导入类型,不会打包 DEFAULT_CONFIG
const myConfig: Config = {
apiUrl: "https://my-api.com",
};

总结#

类型擦除是 TypeScript 的核心特性,理解它对于编写正确的 TypeScript 代码至关重要:

  1. 会被擦除:接口、类型别名、泛型参数、类型注解
  2. 不会被擦除:类、普通枚举(const 枚举会被擦除)
  3. 运行时检查:不能使用 instanceof 检查接口,需要使用类型守卫函数
  4. 最佳实践:使用 import type 来明确纯类型导入,优化打包体积

理解类型擦除可以帮助你:

  • 避免运行时错误
  • 编写更高效的代码
  • 更好地利用 TypeScript 的类型系统

相关阅读:TypeScript 导入导出指南

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

类 型 擦 除 一 定 要 懂
https://l1ngg.info/posts/tech/ts-type-erasure/
作者
L1ngg
发布于
2026-02-02
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00