本文总结了 TypeScript 的类型系统基础,涵盖了:
any
、unknown
、never
)的完整介绍TypeScript 是 JavaScript 的超集,由 Microsoft 于 2012 年发布,设计目标是增强 JavaScript 的开发体验和代码质量。TypeScript 代码最终会被编译(转译)为原生 JavaScript,因此它可以在任何支持 JavaScript 的环境中运行。
TypeScript 与 JavaScript 的关系可以用以下方式理解:
// TypeScript = JavaScript + 静态类型系统 + 额外语言特性
TypeScript 的核心价值在于引入了静态类型检查,这一特性带来了多方面的优势:
静态类型检查通过 TypeScript 编译器(tsc)在开发阶段进行,它分析代码的类型结构并报告潜在问题,无需执行代码即可发现类型不匹配等错误。
TypeScript 支持 JavaScript 的所有原始类型,并提供严格的类型检查:
// 字符串类型
let name: string = "TypeScript";
name = 42; // 错误: 不能将类型"number"分配给类型"string"
// 数字类型 - 包括整数和浮点数
let age: number = 25;
let price: number = 99.99;
let infinity: number = Infinity;
let notANumber: number = NaN; // 即使是 NaN 也是 number 类型
// 布尔类型
let isActive: boolean = true;
isActive = "yes"; // 错误: 不能将类型"string"分配给类型"boolean"
TypeScript 原始类型直接映射到 JavaScript 运行时的原始值,但提供了编译时的类型安全保障。
TypeScript 中定义数组有两种语法:
// 方式 1: 类型后加方括号
let numbers: number[] = [1, 2, 3, 4, 5];
// 方式 2: 使用泛型数组类型
let strings: Array<string> = ["a", "b", "c"];
// 错误示例
numbers.push("six"); // 错误: 类型"string"的参数不能赋给类型"number"的参数
元组是固定长度、元素类型可以不同的数组:
// 定义一个包含字符串和数字的元组
let person: [string, number] = ["Alice", 30];
// 访问已知索引的元素,类型被正确推断
let name: string = person[0]; // 类型是 string
let age: number = person[1]; // 类型是 number
// 错误示例
person[3] = "Bob"; // 错误: 索引超出元组长度
person = ["Bob", "30"]; // 错误: 类型不匹配
TypeScript 4.0 之后,元组类型支持标记和可变长度:
// 带标签的元组
type Person = [name: string, age: number];
let employee: Person = ["Bob", 42];
// 可变长度元组
type StringNumberBooleans = [string, number, ...boolean[]];
let snb: StringNumberBooleans = ["hello", 42, true, false, true];
这三种类型代表了 TypeScript 类型系统中的特殊概念:
any
是 TypeScript 的逃生舱,它绕过类型检查:
let flexible: any = 4;
flexible = "string now";
flexible = { complex: "object" };
flexible.nonExistentMethod(); // 不会报错!
// 污染其他类型
let typedArray: number[] = [1, 2, 3];
let anyValue: any = "string";
typedArray.push(anyValue); // 不会报错,但破坏了类型安全
unknown
是类型安全的 any
:
let safeValue: unknown = 4;
safeValue = "string now";
safeValue = { complex: "object" };
// 错误: 对象的类型为 "unknown"
safeValue.toString();
// 正确使用方式: 先进行类型检查
if (typeof safeValue === "string") {
console.log(safeValue.toUpperCase()); // 安全
}
// 或使用类型断言
console.log((safeValue as string).toUpperCase());
never
表示永远不会有值的类型:
// 返回 never 的函数不能有可达的终点
function throwError(message: string): never {
throw new Error(message);
}
// 无限循环也返回 never
function infiniteLoop(): never {
while (true) {}
}
// never 是所有类型的子类型
function controlFlow(value: string | number) {
if (typeof value === "string") {
// value 是 string 类型
} else if (typeof value === "number") {
// value 是 number 类型
} else {
// value 是 never 类型
// 这个分支在理论上不应该被执行
value; // 类型是 never
}
}
void
主要用于表示函数没有返回值:
// 没有返回值的函数
function logMessage(message: string): void {
console.log(message);
// 不需要 return 语句
}
// void 类型变量只能赋值为 undefined 或 null(在 --strictNullChecks 关闭时)
let unusable: void = undefined;
unusable = null; // 仅在 --strictNullChecks 未启用时有效
这两个类型分别对应 JavaScript 中的 null
和 undefined
值:
// 明确的 null 和 undefined 类型
let n: null = null;
let u: undefined = undefined;
// 在启用 --strictNullChecks 时,null 和 undefined 只能赋值给对应类型或 any/unknown
let s: string = null; // 错误: 不能将类型"null"分配给类型"string"
// 使用联合类型允许 null 或 undefined
let nullable: string | null = "hello";
nullable = null; // 可以
TypeScript 的 --strictNullChecks
标志是一个重要的类型安全特性,它防止将 null
或 undefined
分配给不明确允许这些值的类型。
类型注解是 TypeScript 中显式声明变量、参数或返回值类型的方式:
// 变量类型注解
let counter: number = 0;
// 函数参数和返回值类型注解
function greet(name: string): string {
return `Hello, ${name}!`;
}
// 对象类型注解
let user: { id: number; name: string; active?: boolean } = {
id: 1,
name: "Alice"
};
// 函数类型注解
let callback: (data: string) => void;
callback = (data) => console.log(data);
类型注解最佳实践:
为公共 API 和接口添加类型注解:
// 好的做法 - 公共函数清晰标注类型
export function processData(input: string[]): ProcessedResult {
// 实现...
}
复杂或不明显的类型使用注解:
// 不明显的类型应明确注解
const result: Map<string, User[]> = groupUsersByDepartment(employees);
函数参数总是添加类型注解:
// 参数类型注解,返回值可以推断
function calculateTotal(items: CartItem[]) {
return items.reduce((sum, item) => sum + item.price, 0);
}
避免冗余的类型注解:
// 不好 - 冗余的类型信息
const name: string = "TypeScript";
// 好 - 类型可以被推断
const name = "TypeScript";
TypeScript 的类型推断系统是其核心特性之一,它通过上下文分析推断类型:
变量初始化推断:
// 推断为 number 类型
let counter = 0;
// 推断为 string[] 类型
const names = ["Alice", "Bob", "Charlie"];
函数返回值推断:
// 返回值推断为 number 类型
function add(a: number, b: number) {
return a + b;
}
上下文类型推断:
// 参数 e 被推断为 MouseEvent 类型
document.addEventListener("click", (e) => {
console.log(e.clientX, e.clientY);
});
结构化推断:
// 对象字面量推断
const user = {
id: 1,
name: "Alice",
active: true
};
// user.id 被推断为 number
// user.name 被推断为 string
// user.active 被推断为 boolean
类型推断机制基于 TypeScript 编译器内部的"流分析"(flow analysis)系统:
选择类型推断还是显式标注的一般准则:
场景 | 推荐方式 | 原因 |
---|---|---|
变量通过字面量初始化 | 依赖推断 | 类型明显,推断准确 |
函数返回复杂类型 | 显式标注 | 清晰记录预期输出类型 |
函数参数 | 显式标注 | 提供清晰接口契约 |
类成员变量 | 显式标注 | 明确接口设计 |
空数组或对象 | 显式标注 | 推断为 any[] 或 {} |
公共 API | 显式标注 | 提供清晰的文档 |
内部实现细节 | 依赖推断 | 减少冗余,提高可维护性 |
示例:
// 基本变量初始化 - 依赖推断
const count = 42; // number
const message = "Hello"; // string
const isActive = true; // boolean
// 函数参数和返回值 - 显式标注参数,可以推断简单返回值
function calculateArea(width: number, height: number) {
return width * height;
}
// 复杂返回类型 - 显式标注
function fetchUserData(id: string): Promise<UserProfile> {
// 实现...
}
// 空数组 - 显式标注
const items: CartItem[] = []; // 否则推断为 any[]
// 公共 API - 显式标注
export interface UserService {
findById(id: string): Promise<User>;
update(user: User): Promise<void>;
}
TypeScript 使用结构类型系统(Structural Type System),而非名义类型系统(Nominal Type System)。在结构类型系统中,类型兼容性基于类型的结构(它们包含的成员),而非它们的名称或明确的继承关系。
// 结构类型示例
interface Point {
x: number;
y: number;
}
class Coordinate {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
// 尽管 Coordinate 不是明确继承自 Point,但它们结构兼容
const point: Point = new Coordinate(10, 20); // 有效
在这个例子中,Coordinate
类与 Point
接口结构兼容,因为它们都有相同类型的 x
和 y
属性。
结构类型系统的主要优势:
TypeScript 使用"结构子类型化"来处理对象类型的兼容性:
interface Named {
name: string;
}
// 多出属性的类型可以赋值给少属性的类型
let named: Named;
let person = { name: "Alice", age: 30 };
named = person; // 有效:person 有 name 属性
// 但在对象字面量直接赋值时,会进行额外属性检查
named = { name: "Bob", age: 25 }; // 错误:对象字面量只能指定已知属性
对象兼容性规则:
函数类型兼容性涉及参数类型和返回值类型的比较:
// 返回值类型:源函数的返回类型必须可分配给目标函数的返回类型
type Logger = (message: string) => void;
type StringTransformer = (message: string) => string;
let loggerFunc: Logger;
let transformerFunc: StringTransformer = (message) => message.toUpperCase();
// 有效:string 可以分配给 void
loggerFunc = transformerFunc;
// 参数类型:目标函数的参数必须可分配给源函数的参数
type MouseHandler = (event: MouseEvent) => void;
type EventHandler = (event: Event) => void;
let mouseHandler: MouseHandler;
let eventHandler: EventHandler = (e) => console.log(e.type);
// 有效:MouseEvent 是 Event 的子类型
mouseHandler = eventHandler;
// 但反过来不行
eventHandler = mouseHandler; // 错误:MouseEvent 有 Event 没有的属性
函数兼容性规则:
泛型类型的兼容性取决于泛型参数的使用方式:
// 泛型参数未使用,兼容性不受泛型影响
interface Container<T> {
tag: string;
}
let numberContainer: Container<number> = { tag: "numbers" };
let stringContainer: Container<string> = { tag: "strings" };
// 兼容,因为 T 未在结构中使用
numberContainer = stringContainer;
// 泛型参数被使用,兼容性受泛型影响
interface ValueContainer<T> {
value: T;
}
let numValue: ValueContainer<number> = { value: 123 };
let strValue: ValueContainer<string> = { value: "hello" };
// 不兼容,因为 T 在结构中使用
numValue = strValue; // 错误
类与接口类似,但有两个不同点:
class Animal {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
let animal: Animal;
let dog: Dog;
// private 和 protected 成员会影响兼容性
animal = dog; // 有效
dog = animal; // 错误:Animal 没有 breed 属性
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
// 即使结构相同,不同类中的 protected 成员也被视为不同
animal = new Person("human"); // 错误
类兼容性规则:
TypeScript 类型系统强大而灵活,它不仅能捕获常见错误,还能作为代码文档,提升开发效率和代码质量。