揭秘:TypeScript 类型系统是如何给代码穿上 “防弹衣” 的

**

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的类型检查起到帮助~

相关推荐
欢乐小v15 分钟前
elementui-admin构建
前端·javascript·elementui
霸道流氓气质42 分钟前
Vue中使用vue-3d-model实现加载3D模型预览展示
前端·javascript·vue.js
溜达溜达就好1 小时前
ubuntu22 npm install electron --save-dev 失败
前端·electron·npm
慧一居士1 小时前
Axios 完整功能介绍和完整示例演示
前端
晨岳1 小时前
web开发-CSS/JS
前端·javascript·css
22:30Plane-Moon1 小时前
前端之CSS
前端·css
半生过往1 小时前
前端上传 pdf 文件 ,前端自己解析出来 生成界面 然后支持编辑
前端·pdf
晨岳1 小时前
web开发基础(CSS)
前端·css
.又是新的一天.1 小时前
前端-CSS (样式引入、选择器)
前端·css