**
TypeScript 的类型系统就像一张精密的网,能在代码运行前就过滤掉大部分类型错误。刚开始用的时候总觉得是在给代码 "画地为牢",但熟悉之后才发现,这张网其实是在给代码保驾护航。今天就来好好拆解一下,TS 是怎么给各种数据类型加上约束的,以及使用时要避开哪些坑。
一、基础数据类型
JavaScript 的基础类型有 number、string、boolean、null、undefined、symbol、bigint 这七种,TypeScript 对它们的类型检查直接又明确。
1. 显式类型注解
最直接的方式就是在变量声明时加上类型注解,用 :
连接变量名和类型:
ts
let age: number = 25;
let name: string = "张三";
let isStudent: boolean = true;
let uid: symbol = Symbol("unique");
let bigNum: bigint = 100n;
一旦指定类型,变量就不能再接收其他类型的值:
ts
age = "25"; // 报错:类型"string"不能赋值给类型"number"
2. 特殊的 null 和 undefined
null 和 undefined 比较特殊,它们既是值也是类型:
ts
let empty: null = null;
let notDefined: undefined = undefined;
在默认的严格模式下,null 和 undefined 不能赋值给其他类型:
ts
let num: number = null; // 报错,严格模式下不允许
如果想允许这种情况,可以在 tsconfig.json 里把 strictNullChecks
设为 false,或者用联合类型:
ts
let num: number | null = null; // 合法
num = 123; // 也合法
3. 类型推断的妙用
其实很多时候不用显式写类型注解,TS 会根据初始值自动推断类型:
ini
let count = 10; // 推断为 number 类型
count = "10"; // 报错
let message = "hello"; // 推断为 string 类型
这种 "无感化" 的类型检查,既保留了 JS 的灵活,又不失严谨。
二、引用数据类型
引用类型(对象、数组、函数等)的类型检查要复杂得多,TS 为此设计了一套完整的类型描述方案。
1. 对象类型
描述对象类型时,需要指定每个属性的类型:
ts
let user: {
name: string;
age: number;
isAdmin?: boolean; // 可选属性,加 ? 表示可以不存在
} = {
name: "李四",
age: 30
// 可选属性可以不写
};
如果访问对象上不存在的属性,TS 会立刻报错:
ts
user.gender; // 报错:属性"gender"不存在于类型"{...}"上
还可以用 readonly 标记只读属性,防止被修改:
ts
let config: {
readonly apiUrl: string;
port: number;
} = {
apiUrl: "https://api.example.com",
port: 8080
};
config.apiUrl = "new url"; // 报错:只读属性不能修改
2. 数组类型
数组的类型注解有两种写法,推荐用 类型[]
的形式:
ts
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"]; // 泛型写法
数组中的元素必须符合指定类型:
ts
numbers.push("4"); // 报错:不能把 string 放进 number 数组
对于多维数组,可以这样写:
ts
let matrix: number[][] = [
[1, 2],
[3, 4]
]; // 二维数组
3. 函数类型
函数的类型检查要同时关注参数和返回值:
ts
// 函数声明式
function add(a: number, b: number): number {
return a + b;
}
// 函数表达式
const multiply: (x: number, y: number) => number = (x, y) => {
return x * y;
};
参数可以设置默认值,带默认值的参数会被自动识别为可选参数:
ts
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}`;
}
greet("张三"); // 第二个参数用默认值
剩余参数的类型要用数组表示:
ts
function sum(...nums: number[]): number {
return nums.reduce((total, num) => total + num, 0);
}
三、高级类型
当基础类型满足不了需求时,TS 的高级类型就能派上用场了。
1. 联合类型
用 |
表示变量可以是多种类型中的一种:
ts
let value: string | number;
value = "hello"; // 合法
value = 123; // 合法
value = true; // 报错:不在允许的类型范围内
常见用途是处理可能为 null 的值:
ts
function printLength(str: string | null) {
// 必须先判断类型才能安全使用
if (str !== null) {
console.log(str.length);
} else {
console.log("空值");
}
}
2. 交叉类型
用 &
表示变量要同时满足多个类型的特征:
ts
type Student = {
id: number;
name: string;
};
type Worker = {
company: string;
salary: number;
};
// 既要是学生也要是工作者
type StudentWorker = Student & Worker;
let sw: StudentWorker = {
id: 1,
name: "王五",
company: "xxx",
salary: 5000
}; // 必须包含所有属性
3. 接口
当对象类型比较复杂时,用接口(interface)来复用类型定义:
ts
interface Product {
id: number;
name: string;
price: number;
discount?: number; // 可选属性
}
// 接口可以被继承
interface DigitalProduct extends Product {
downloadUrl: string;
fileSize: number;
}
let ebook: DigitalProduct = {
id: 101,
name: "TS 入门指南",
price: 59,
downloadUrl: "/books/ts-guide",
fileSize: 2048
};
接口和类型别名(type)很像,但接口可以重复声明来扩展:
ts
interface User {
name: string;
}
interface User {
age: number;
}
let user: User = {
name: "赵六",
age: 28
};
4. 枚举
枚举(enum)适合表示有明确取值范围的场景,比如状态码:
ts
enum OrderStatus {
PENDING, // 默认值 0
PAID, // 1
SHIPPED, // 2
DELIVERED // 3
}
let status: OrderStatus = OrderStatus.PAID;
枚举值可以手动指定,后续值会自动递增:
ts
enum Priority {
LOW = 1,
MEDIUM, // 2
HIGH // 3
}
也可以用字符串枚举,更具可读性:
ts
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT"
}
function request(url: string, method: HttpMethod) {
// ...
}
request("/users", HttpMethod.GET); // 比直接传字符串更安全
5.泛型
泛型就像类型层面的 "函数参数",让我们能创建可复用的、适用于多种类型的组件,同时保持类型安全。它解决了 "既要通用又要类型严格" 的矛盾。
(1)泛型的基本用法
用 <T>
(T 是约定俗成的名称,也可以用 U、V 等)表示 "类型变量",在定义时不确定具体类型,使用时再指定:
ts
// 定义一个泛型函数,返回传入的参数
function identity<T>(value: T): T {
return value;
}
// 使用时自动推断类型
const num: number = identity(123);
const str: string = identity("hello");
// 也可以显式指定类型
const bool: boolean = identity<boolean>(true);
这里的 T 会根据传入的参数类型动态 "填充",既保证了函数的通用性,又让输入和输出的类型严格一致。
(2)泛型在集合中的应用
处理数组、对象等集合类型时,泛型能精确描述元素类型:
ts
// 泛型数组
function getFirstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const numbers = [1, 2, 3];
const firstNum: number = getFirstElement(numbers);
const strings = ["a", "b", "c"];
const firstStr: string = getFirstElement(strings);
(3)泛型约束:给类型变量设边界
如果需要访问泛型参数的某个属性,可以用 extends 约束类型范围:
ts
// 约束 T 必须有 length 属性
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(value: T): T {
console.log(value.length);
return value;
}
logLength("hello");
logLength([1, 2, 3]);
logLength(123); // 报错,数字没有 length
(4)泛型接口与类
泛型也能用于接口和类,让它们成为 "类型模板":
ts
// 泛型接口:定义一个键值对结构
interface KeyValuePair<K, V> {
key: K;
value: V;
}
// 使用时指定具体类型
const pair1: KeyValuePair<number, string> = { key: 1, value: "a" };
const pair2: KeyValuePair<string, boolean> = { key: "isValid", value: true };
// 泛型类:一个简单的栈结构
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push("2"); // 报错,只能推 number 类型
(5)泛型工具类型
TS 内置了很多基于泛型的工具类型,简化常见操作:
-
Partial:将 T 的所有属性转为可选
-
Readonly:将 T 的所有属性转为只读
-
Pick<T, K>:从 T 中挑选出 K 对应的属性
ts
interface User {
name: string;
age: number;
}
type PartialUser = Partial<User>;
// 等价于 { name?: string; age?: number }
type ReadonlyUser = Readonly<User>;
// 等价于 { readonly name: string; readonly age: number }
四、特殊类型
有些场景下需要打破严格的类型约束,TS 提供了几个特殊类型来应对。
1. any
any 类型可以接收任何类型的值,也可以被赋值给任何类型,相当于关闭了 TS 的类型检查:
ts
let anything: any = "hello";
anything = 123; // 合法
anything = true; // 合法
let num: number = anything; // 不报错
用 any 虽然灵活,但会失去 TS 的保护,尽量少用。常见用途是处理动态数据(比如 JSON 解析的未知结构):
ts
const data: any = JSON.parse('{"name": "test"}');
2. unknown
unknown 是比 any 更安全的类型,它可以接收任何类型的值,但不能直接赋值给其他类型,必须先做类型检查:
ts
let value: unknown = "hello";
let str: string = value; // 报错:不能直接赋值
// 先判断类型才能使用
if (typeof value === "string") {
str = value; // 合法
}
3. never
never 表示那些永远不会出现的值,比如抛出异常的函数:
ts
function throwError(message: string): never {
throw new Error(message);
// 永远不会执行到 return
}
或者无限循环的函数:
ts
function infiniteLoop(): never {
while (true) {
// ...
}
}
never 常用在类型守卫中做全面性检查:
ts
type OnlyStringOrNumber = string | number;
function handleValue(value: OnlyStringOrNumber) {
if (typeof value === "string") {
// 处理字符串
} else if (typeof value === "number") {
// 处理数字
} else {
// 如果漏了类型,这里会报错
const _exhaustiveCheck: never = value;
}
}
五、类型使用的注意事项
1. 不要过度使用 any
很多人 TS 时,遇到类型报错就用 any 解决,这相当于白用了 TS。比如处理后端返回的数据时,应该定义接口而不是用 any:
ts
// 不好的做法
interface ApiResponse {
data: any; // 丢失类型信息
}
// 好的做法
interface UserData {
id: number;
name: string;
}
interface ApiResponse {
data: UserData;
}
2. 注意 null 和 undefined
默认开启 strictNullChecks 后,null 和 undefined 不会被自动视为其他类型的子类型。访问可能为 null 的属性时,必须先做判断:
ts
function getLength(str: string | null) {
// 错误写法:可能为 null 时不能直接访问属性
// return str.length;
// 正确写法
return str?.length ?? 0;
}
3. 区分类型和接口
类型别名(type)和接口(interface)很多时候可以互换,但有几个区别:
-
接口可以重复声明扩展,类型别名不行
-
类型别名可以表示基本类型、联合类型等,接口只能表示对象类型
-
接口支持继承,类型别名需要用交叉类型模拟继承
一般来说,描述对象结构优先用接口,其他情况用类型别名。
4. 避免 "类型膨胀"
不要为了追求 "绝对类型安全" 而定义过于复杂的类型。比如一个简单的配置对象,没必要写成:
ts
// 过度设计
interface Config {
appName: string;
port: number | string;
enabled: boolean;
// ... 20 个属性
}
// 其实可以先简单定义,后续再扩展
type SimpleConfig = {
appName: string;
[key: string]: any; // 允许其他任意属性
};
类型系统是工具,不是目的
TypeScript 的类型系统再强大,最终也是为了写出更可靠的代码。刚开始可能会觉得繁琐,但熟悉之后会发现,这些类型注解其实是在帮我们梳理代码逻辑。
不要为了用类型而用类型,也不要因为暂时的麻烦就放弃类型检查。找到适合自己项目的类型严格程度,才能让 TypeScript 真正发挥作用。
希望本文对你掌握TypeScript的类型检查起到帮助~