TypeScript 的类型系统看似简单,但当 undefined、null、void、never 同时出现在错误信息中时,你是否也会感到过困惑?本篇文章将深入 TypeScript 的类型层次,揭开每个基础类型背后的秘密。
TypeScript的类型层次结构
在深入具体类型前,让我们先理解TypeScript的类型层次结构:
从上图中,我们可以看出:
- 顶层类型:any & unknown:任何值都可以赋值给它。
- 基础类型:number & bigint & boolean & string & symbol:构建一切的基石。
- 对象类型:object & array & function。
- 特殊类型:enum & tuple。
- 空类型:null & undefined & void & never:边界情况,4种不同的表示为“空”的类型。
- 底层类型:never:最特殊的一种空类型,没有值可以赋值给它。
顶层类型
any:类型系统的"逃生舱"
any 类型是所有类型的“教父”,会放弃所有类型检查,所以也被称为类型系统的"逃生舱"。为达目的,它可以不惜一切代价,因此 any 类型通常会作为代码兜底的类型,即我们和 TypeScript 类型检测器确实是无法确实类型是什么。在实际开发中,any 类型一定要慎用,除非不得已。
// any放弃所有类型检查
let dangerous: any = "hello world";
dangerous = 42; // 可以重新赋值为任意类型
dangerous.nonExistentMethod(); // 编译通过,运行时崩溃!
dangerous.toFixed(); // 编译通过,但string没有toFixed方法
上述代码可以看出,在使用 any 类型时,会存在一定的风险,那么在何时使用 any 比较合适呢?
- 迁移JavaScript项目时的临时方案。
- 处理第三方库没有类型定义的情况。
- 编写测试时快速原型。
unknown:类型安全的any
如果 any 是教父,那么 unknown 就可以理解成是一个“卧底”,与坏人同流合污,但却是好人一边的。当我们确实无法预知一个值的类型时,不要使用 any,可以使用 unknown。
// unknown保持类型检查
let safe: unknown = "hello world";
safe = 42; // 可以重新赋值
// safe.toFixed(); // 编译错误:需要类型收窄
// 必须进行类型守卫
if (typeof safe === "number") {
console.log(safe.toFixed(2)); // 类型收窄为number
}
对于 any 和 unknown 类型的使用,在后面会专门写一篇文章进行详细讲解。
基础类型全解
number
number 包括所有的数字:整数、浮点数、正数、负数、Infinity 、 NaN等。值得注意的是,JavaScript 中不存在真正意义上的整数,所有的数字都是浮点数。
// 所有数字类型都是number
const decimal: number = 6;
const hex: number = 0xf00d; // 十六进制
const binary: number = 0b1010; // 二进制
const octal: number = 0o744; // 八进制
const float: number = 3.14;
const infinity: number = Infinity;
const notANumber: number = NaN;
const big: number = 1_000_000; // 数值分隔符
// 注:JavaScript的数字都是浮点数
console.log(0.1 + 0.2); // 0.30000000000000004
// 数字字面量类型(TypeScript的强大特性)
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
function rollDice(): Dice {
return 3; // 只能返回1-6
}
bigint
在 JavaScript中,number 类型能表示的最大整数为2^53,而 bigint 能表示的数比这个要大得多,并且它能表示真正的大整数,我们也不用再担心精度丢失问题。
// bigint可以安全表示任意大的整数
const big: bigint = 9007199254740991n;
const huge: bigint = BigInt(9007199254740991);
// 与number不兼容
const normal: number = 100;
// const mixed: bigint = normal; // 类型错误
const converted: bigint = BigInt(normal); // 编译通过
boolean
boolean 类型的值有两个:true 和 false。
// 基本的布尔类型
const isActive: boolean = true;
const isDone = false; // 类型推断为boolean
string
string 类型包含所有的字符串,以及可以对字符串执行的操作:
// 基本字符串类型
const name: string = "TypeScript";
const greeting = `Hello, ${name}!`; // 模板字符串
对于上述的几种基本数据类型,在实际开发中并不推荐声明变量类型。因为对于基本数据类型,TypeScript 是怎么自动推断出具体的数据类型,显式类型声明反而是多余的:
let x: number = 10; // 不要这么写let x = 10; // 推荐这样写
symbol和unique symbol
symbol 是 ES6+ 中新引入的语言特性;unique symbol 是TypeScript的类型级唯一性,两者都用于表示真正的唯一性。
symbol:运行时的唯一标识
// 每个Symbol都是唯一的
const sym1 = Symbol("description");
const sym2 = Symbol("description");
console.log(sym1 === sym2); // false
// 作为对象键(不会意外覆盖)
const uniqueKey = Symbol("user:id");
const user = {
[uniqueKey]: 123,
name: "Alice"
};
console.log(user[uniqueKey]); // 123
console.log(Object.keys(user)); // ["name"],symbol键不可枚举
unique symbol:编译时的唯一性保证
// unique symbol有独特的类型
const Brand: unique symbol = Symbol("Brand");
// 品牌类型模式:防止类型混淆
type UserId = number & { readonly [Brand]: true };
type ProductId = number & { readonly [Brand]: true };
function createUserId(id: number): UserId {
return id as UserId;
}
function createProductId(id: number): ProductId {
return id as ProductId;
}
const userId = createUserId(123);
const productId = createProductId(123);
// TypeScript阻止误用
function processUser(id: UserId) { /* ... */ }
processUser(userId); // 正常运行
processUser(productId); // 类型错误
processUser(123); // 类型错误
四大"空值"类型
null vs undefined:JavaScript的双重空值
- null:有意的空值。
- undefined:未定义的值。
// null:有意的空值
let explicitNull: string | null = null;
// undefined:未定义的值
let notInitialized: string | undefined;
// 可选参数和属性
function greet(name?: string) {
// name的类型是 string | undefined
console.log(`Hello, ${name ?? "Guest"}`);
}
interface User {
id: number;
name: string;
middleName?: string; // string | undefined
}
void vs never:函数的返回类型
- void:正常执行但无返回值。
- never:永不返回的函数。
// 场景1:总是抛出错误
function throwError(message: string): never {
throw new Error(message);
}
// 场景2:无限循环
function infiniteLoop(): never {
while (true) {
// 永不结束
}
}
// 场景3:穷尽性检查(Exhaustiveness checking)
type Shape = "circle" | "square" | "triangle";
function getArea(shape: Shape): number {
switch (shape) {
case "circle": return Math.PI * 1;
case "square": return 1;
case "triangle": return 0.5;
default:
// 如果Shape类型扩展,这里会报错
const exhaustiveCheck: never = shape;
throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
}
}
// 场景4:类型运算中的不可能
type NonNullable<T> = T extends null | undefined ? never : T;
type ExtractStrings<T> = T extends string ? T : never;
对象类型
object
在 TypeScript 中,object 类型用于表示非原始类型的值,仅比any 类型的范围窄一点,但也窄不了多少。object 类型对值知之甚少,只能表示该值是一个 JavaScript 对象,而不是 null。
declare function create(o: object | null): void;
create({ prop: 0 }); // ✅ 对象
create(null); // ✅ null
create([]); // ✅ 数组
create(() => {}); // ✅ 函数
create(42); // ❌ 原始类型
create("string"); // ❌ 原始类型
// 但object太宽泛,通常使用更具体的类型
interface Config {
apiUrl: string;
timeout: number;
}
// 使用Record表示键值对
type Headers = Record<string, string>;
array
和 JavaScript 中一样,TypeScript 中的数组也是特殊类型的对象:
// 两种写法等价
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
// 只读数组
const readonlyList: readonly number[] = [1, 2, 3];
// readonlyList.push(4); // ❌ 编译错误
// 多维数组
let matrix: number[][] = [
[1, 2, 3],
[4, 5, 6]
];
// 数组泛型方法
function reverse<T>(array: T[]): T[] {
return [...array].reverse();
}
function
和 JavaScript 中一样,TypeScript 中的 function 也是特殊类型的对象:
// 函数类型声明
type GreetFunction = (name: string) => string;
type BinaryOperator = (a: number, b: number) => number;
// 函数实现
const greet: GreetFunction = (name) => `Hello, ${name}!`;
const add: BinaryOperator = (a, b) => a + b;
// 可选参数和默认参数
function createUser(name: string, age?: number, isAdmin = false) {
// age: number | undefined
// isAdmin: boolean
}
// 剩余参数
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
// 函数重载(提供更好的类型提示)
function format(input: string): string;
function format(input: number): string;
function format(input: string | number): string {
if (typeof input === "string") {
return input.toUpperCase();
}
return input.toFixed(2);
}
对于函数重载,在后面会专门写一篇文章进行详细讲解。
tuple
tuple 类型是 array类型的子类型,表示为元组,是数组的一种特殊方式:长度固定,各索引位上的值具有固定的已知类型。在声明元组时,必须显式注解类型。
// 基本元组
let pair: [string, number] = ["hello", 42];
// 可选元素
let optionalTuple: [string, number?] = ["hello"];
optionalTuple = ["hello", 42];
// 带标签的元组(TypeScript 4.0+)
let user: [name: string, age: number, email?: string] = ["Alice", 25];
// 剩余元素
type StringNumberPair = [string, ...number[]];
const snp: StringNumberPair = ["hello", 1, 2, 3];
enum
enum 类型的作用是枚举类型中包含的各个值,是一种无序的数据结构,把键映射到值上。可以把枚举理解成键固定的对象:
// 数字枚举(默认)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
// 字符串枚举
enum LogLevel {
Info = "INFO",
Warn = "WARN",
Error = "ERROR"
}
// 常量枚举(编译时完全删除)
const enum ButtonType {
Primary,
Secondary,
Danger
}
// 异构枚举(混合字符串和数字,不推荐)
enum Confusing {
Yes = 1,
No = "NO"
}
// 现代替代方案:联合类型
type ModernDirection = "up" | "down" | "left" | "right";
type ModernLogLevel = "INFO" | "WARN" | "ERROR";
结语
本文介绍了 TypeScript 中的数据类型,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!