TypeScript基础类型全解

64 阅读5分钟

TypeScript 的类型系统看似简单,但当 undefined、null、void、never 同时出现在错误信息中时,你是否也会感到过困惑?本篇文章将深入 TypeScript 的类型层次,揭开每个基础类型背后的秘密。

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 中的数据类型,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!