1. 类型系统基础认知
当我们在JavaScript(JS)和TypeScript(TS)之间切换时,最直观的感受就是类型系统的引入。JS作为动态类型语言,变量的类型可以在运行时改变,这种灵活性带来了便利,但也埋下了隐患。TS通过静态类型检查,在编译阶段就能发现潜在的类型错误,这是两者最本质的区别。
在JS中,我们常用的基本类型包括:
- 原始类型:number、string、boolean、null、undefined、symbol(ES6新增)
- 对象类型:Object、Array、Function等
TS在此基础上扩展了类型系统,除了兼容JS的所有类型外,还增加了:
- 元组(Tuple)
- 枚举(Enum)
- Any/Unknown
- Void/Never
- 字面量类型
- 交叉类型(&)和联合类型(|)
提示:从JS迁移到TS时,建议先掌握基础类型的使用,再逐步学习高级类型特性。类型系统是TS的核心,但不必一开始就追求复杂的类型编程。
2. TS内置类型深度解析
2.1 原始类型与JS的异同
TS中的原始类型与JS基本对应,但有着更严格的类型检查:
typescript复制// JS中可以这样做
let num = 123;
num = "hello"; // 运行时才会发现错误
// TS中会直接报错
let num: number = 123;
num = "hello"; // Type 'string' is not assignable to type 'number'
特别需要注意的是null和undefined在TS中的表现。在严格模式下(strictNullChecks: true),它们只能赋值给any和各自的类型:
typescript复制let str: string;
str = null; // 严格模式下报错
str = undefined; // 严格模式下报错
2.2 数组与元组
TS中数组有两种表示方式:
typescript复制// 方式一:元素类型后接[]
let arr1: number[] = [1, 2, 3];
// 方式二:使用泛型Array<元素类型>
let arr2: Array<number> = [1, 2, 3];
元组(Tuple)是TS特有的类型,它允许表示一个已知元素数量和类型的数组:
typescript复制let tuple: [string, number];
tuple = ["hello", 10]; // 正确
tuple = [10, "hello"]; // 错误
注意:虽然元组有固定长度和类型,但通过push等方法仍然可以突破这个限制,这是设计上的妥协。
2.3 枚举类型详解
枚举(Enum)是TS对JS的扩展,它为一组数值赋予友好的名字:
typescript复制enum Direction {
Up = 1,
Down,
Left,
Right
}
let dir: Direction = Direction.Up;
枚举在编译后会生成一个双向映射的对象:
javascript复制// 编译后的JS代码
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 1] = "Up";
Direction[Direction["Down"] = 2] = "Down";
// ...
})(Direction || (Direction = {}));
2.4 特殊类型:Any、Unknown、Void、Never
Any: 任意类型,相当于关闭类型检查Unknown: 类型安全的any,使用前需要类型断言或类型收窄Void: 表示没有返回值的函数Never: 表示永远不会返回的函数(如抛出异常或死循环)
typescript复制function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
3. 类型进阶与类型操作
3.1 类型别名与接口
类型别名(Type Alias)和接口(Interface)都可以用来定义对象类型:
typescript复制// 类型别名
type Point = {
x: number;
y: number;
};
// 接口
interface Point {
x: number;
y: number;
}
它们的区别在于:
- 接口可以extends和implements,类型别名不行
- 类型别名可以使用联合类型、交叉类型等更复杂的类型表达式
- 接口可以合并声明(声明合并),类型别名不行
3.2 联合类型与类型守卫
联合类型(Union Types)表示一个值可以是几种类型之一:
typescript复制function padLeft(value: string, padding: string | number) {
// ...
}
处理联合类型时,通常需要使用类型守卫(Type Guard)来缩小类型范围:
typescript复制// typeof类型守卫
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
// instanceof类型守卫
if (error instanceof Error) {
console.log(error.message);
}
// 自定义类型守卫
function isNumber(x: any): x is number {
return typeof x === "number";
}
3.3 类型推断与类型断言
TS具有强大的类型推断能力,大多数情况下不需要显式标注类型:
typescript复制let x = 3; // 推断为number
let y = [0, 1, null]; // 推断为(number | null)[]
当需要覆盖TS的类型推断时,可以使用类型断言:
typescript复制// 尖括号语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// as语法(JSX中必须使用这种)
let strLength: number = (someValue as string).length;
注意:类型断言不是类型转换,它只是在编译阶段起作用,不会影响运行时的行为。
4. 实用类型工具
4.1 内置工具类型
TS提供了一些内置的工具类型:
typescript复制// Partial<T> - 将所有属性变为可选
interface Todo {
title: string;
description: string;
}
type PartialTodo = Partial<Todo>;
// Readonly<T> - 将所有属性变为只读
type ReadonlyTodo = Readonly<Todo>;
// Pick<T, K> - 从T中选取一组属性K
type TodoPreview = Pick<Todo, "title">;
// Record<K, T> - 构造一个类型,其属性名的类型为K,属性值的类型为T
type PageInfo = {
title: string;
};
type Page = "home" | "about" | "contact";
const nav: Record<Page, PageInfo> = {
home: { title: "Home" },
about: { title: "About" },
contact: { title: "Contact" }
};
4.2 条件类型与infer
条件类型(Conditional Types)允许我们根据条件选择类型:
typescript复制type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
结合infer关键字,可以实现更复杂的类型操作:
typescript复制type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function foo() { return 123; }
type FooReturn = ReturnType<typeof foo>; // number
4.3 映射类型
映射类型(Mapped Types)允许基于旧类型创建新类型:
typescript复制// 将所有属性变为只读
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 将所有属性变为可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 添加新属性
type WithNewProp<T> = {
[P in keyof T]: T[P];
} & { newProp: string };
5. 常见问题与解决方案
5.1 类型声明文件(.d.ts)
当使用第三方JS库时,通常需要类型声明文件来获得TS支持:
typescript复制// 全局类型声明
declare module "some-module" {
export function doSomething(): void;
}
// 为已有变量添加类型
declare const MY_GLOBAL: string;
提示:大多数流行库的类型声明都可以通过
@types/库名安装,如@types/lodash。
5.2 类型兼容性陷阱
TS的类型系统是结构化的(基于形状),而不是名义化的(基于名称):
typescript复制interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
p = new Person(); // 正确,因为结构兼容
这种设计带来了灵活性,但也可能导致意料之外的行为:
typescript复制interface Point {
x: number;
y: number;
}
interface Point3D {
x: number;
y: number;
z: number;
}
let p1: Point = { x: 1, y: 2 };
let p2: Point3D = { x: 1, y: 2, z: 3 };
p1 = p2; // 正确(多余属性检查只在对象字面量时触发)
p2 = p1; // 错误(缺少z属性)
5.3 泛型使用技巧
泛型是TS中强大的工具,但使用时需要注意:
typescript复制// 基本泛型函数
function identity<T>(arg: T): T {
return arg;
}
// 泛型约束
function loggingIdentity<T extends { length: number }>(arg: T): T {
console.log(arg.length);
return arg;
}
// 泛型类
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
// 泛型默认类型
function createArray<T = string>(length: number, value: T): Array<T> {
return Array(length).fill(value);
}
在实际项目中,我通常会为复杂组件或工具函数编写泛型类型,这能显著提高代码的复用性和类型安全性。例如,一个通用的API响应类型:
typescript复制interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
timestamp: number;
}
// 使用时
type UserResponse = ApiResponse<{
id: string;
name: string;
age: number;
}>;
从JS迁移到TS是一个渐进的过程,建议从基础类型开始,逐步掌握更高级的类型特性。类型系统是TS最强大的武器,但也要避免过度设计。在实际项目中,我通常会遵循"渐进式类型化"原则:先确保核心业务逻辑的类型安全,再逐步完善其他部分的类型定义。