你写的 TypeScript,其实只是穿了件类型外套的 JavaScript

很多人学了半年 TS,代码里清一色 any,偶尔来个 string | number,然后在简历上写"熟练使用 TypeScript"。这篇文章,就是为了让你彻底告别这种状态。

先说清楚一件事:TypeScript 的类型系统到底在解决什么问题

JavaScript 是动态类型语言。变量可以今天是字符串,明天是数字,后天变成 undefined。这在小项目里无所谓,但一旦代码规模上来------比如一个有 50 个接口、20 个开发者协作的中大型前端项目------没有类型约束,就像在没有红绿灯的十字路口开车,每一次函数调用都是一次赌博

TypeScript 的本质是在编译阶段拦截这些赌局。它不改变运行时行为,但能在你写代码的那一刻,就告诉你哪里会出问题。

理解了这一点,再来看各种类型,你就不会觉得它们是语法糖,而是协议------你和编译器之间的协议,你和团队成员之间的协议。

一、基础类型:别觉得简单,细节决定成败

string / number / boolean

这三个是 TS 里用得最多的类型,也是最容易被忽视的。

ts 复制代码
let name: string = "Andy";
let age: number = 18;
let isLogin: boolean = false;

看起来平平无奇,但"类型安全"的价值在这里:

ts 复制代码
let name: string = "andy";
name.toUpperCase(); // ✅ 合法,string 有这个方法
name.toFixed();     // ❌ 直接报错,string 没有 toFixed

换成纯 JS,这个错误只会在运行时才被发现------可能是用户触发了某个边界条件,可能是在生产环境,可能是在凌晨三点。TS 把运行时错误前移到了编写时,这是它最核心的价值。

null 和 undefined 的处理是门学问

很多项目踩过这样的坑:后端接口返回的某个字段"理论上有值",但有时候会是 null。如果你的类型定义是 string,编译器不会报错,但运行时一旦拿到 null 去调用字符串方法,直接崩。

正确姿势:

ts 复制代码
interface User {
  nickname: string | null; // 明确告诉所有人:这个字段可能为空
}

这样当你试图直接调用 user.nickname.toUpperCase() 时,编译器会强制你先处理 null 的情况。这不是麻烦,这是把锅甩给编译器而不是留给用户

bigint:什么时候才需要它

JavaScript 的 number 类型基于 IEEE 754 双精度浮点数,能精确表示的最大整数是 2^53 - 1,也就是 9007199254740991。超过这个数,精度会丢失。

ts 复制代码
let big: bigint = 9007199254740991n; // 注意末尾的 n

金融系统、密码学、需要处理超大 ID 的场景才会用到它。普通业务开发遇到的机会不多,但知道它存在,不会在某天看到 n 结尾的数字一脸懵。

二、字面量类型与联合类型:从"能用"到"好用"的关键一跳

字面量类型

ts 复制代码
type Direction = "left" | "right" | "up" | "down";

function move(dir: Direction) {
  // ...
}

如果参数类型是 string,你传 "diagonal" 进去编译器不会说话。但用字面量联合类型,"diagonal" 直接飘红。类型越窄,保护越强

这个思路很重要:在你确定某个值只会是有限几种可能的时候,不要偷懒用 string,用字面量联合类型把范围锁死。

联合类型与类型缩小(Type Narrowing)

联合类型(Union)表示"这个值可能是 A,也可能是 B":

ts 复制代码
function print(val: string | number) {
  if (typeof val === "string") {
    console.log(val.toUpperCase()); // 这里 TS 已经知道 val 是 string
  } else {
    console.log(val.toFixed(2));    // 这里 TS 已经知道 val 是 number
  }
}

这叫类型缩小(Type Narrowing)------通过条件判断,编译器会在不同分支里自动推断出更精确的类型。理解这个机制,是写出干净 TS 代码的前提。

交叉类型(Intersection)

联合是"或",交叉是"且":

ts 复制代码
type User = { name: string; email: string };
type Admin = User & { role: "admin"; permissions: string[] };

Admin 必须同时满足 User 和后面那个对象的结构。在实际项目里,这是组合模块类型的利器,比继承更灵活,比重新定义更省力。

三、any、unknown、never:三个经常被误用的类型

any:能不用就不用

ts 复制代码
let x: any;
x.foo.bar.baz(); // 不报错,但运行时爆炸

any 是类型系统的逃生舱。它告诉编译器"别管我,我自己负责"。偶尔处理真的无法预知结构的数据,或者接入没有类型声明的第三方库,可以用。但如果你的代码里 any 满天飞,TypeScript 就成了摆设------你得到了所有 TS 的编译复杂度,却没有得到任何类型安全。

unknown:any 的负责任替代品

unknown 同样表示"不知道是什么类型",但它要求你在使用前必须先做类型检查

ts 复制代码
let x: unknown;

if (typeof x === "string") {
  x.toUpperCase(); // ✅ 通过检查后才能用
}

x.toUpperCase(); // ❌ 直接报错

处理外部输入、API 响应、用户数据时,unknown 是比 any 更安全的选择。

never:表示"这里不应该被执行到"

never 有两个核心用途:

1. 表示函数不会正常返回

ts 复制代码
function throwError(msg: string): never {
  throw new Error(msg);
}

2. 穷举检查(Exhaustive Check)------这个才是精髓

ts 复制代码
type Direction = "up" | "down" | "left";

function move(dir: Direction) {
  if (dir === "up") { /* ... */ }
  else if (dir === "down") { /* ... */ }
  else if (dir === "left") { /* ... */ }
  else {
    const _check: never = dir;
    // 如果未来 Direction 加了 "right" 但这里没处理
    // 编译器会在这行报错,提醒你补全逻辑
  }
}

这是一个防御性编程技巧:让编译器帮你检查所有情况是否都被覆盖。在处理状态机、分支逻辑密集的业务代码里,能避免非常隐蔽的 bug。

四、泛型:类型系统的"函数"

泛型(Generic)是 TypeScript 类型系统里最有表达力的特性。你可以把它理解成类型层面的参数------函数接受值的参数,泛型接受类型的参数。

ts 复制代码
function identity<T>(value: T): T {
  return value;
}

identity<string>("hello"); // 返回类型是 string
identity<number>(42);      // 返回类型是 number

进一步,泛型可以加约束:

ts 复制代码
function getLength<T extends { length: number }>(val: T): number {
  return val.length;
}

getLength("hello");    // ✅ string 有 length
getLength([1, 2, 3]);  // ✅ array 有 length
getLength(123);        // ❌ number 没有 length,报错

泛型约束用 extends 关键字,表示"T 必须满足某个结构"。这让你写出的工具函数既灵活又安全。

五、工具类型:不要重复造轮子

TS 内置了一批工具类型(Utility Types) ,专门用于对已有类型进行变形。掌握这些,能让你的类型定义简洁一个量级。

Partial:把所有字段变成可选

ts 复制代码
interface User {
  id: string;
  name: string;
  email: string;
}

type UpdateUserPayload = Partial<User>;
// 等价于 { id?: string; name?: string; email?: string }

更新接口往往只需要传部分字段,Partial 比重新定义一个新 interface 优雅得多。

Pick 和 Omit:精确裁剪类型

ts 复制代码
type UserPreview = Pick<User, "id" | "name">;
// 只保留 id 和 name

type PublicUser = Omit<User, "password" | "internalNotes">;
// 去掉敏感字段

这两个是一对互补工具。前端展示层经常需要的"脱敏版接口类型",用 Omit 一行搞定。

Record:快速定义映射结构

ts 复制代码
const userCache: Record<string, User> = {};
// 等价于 { [key: string]: User }

Record<K, V> 比写索引签名更直观。后台管理系统里的权限映射、字典数据、配置对象,Record 用起来非常顺手。

Exclude 和 Extract:在联合类型里做集合运算

ts 复制代码
type Status = "active" | "inactive" | "banned";

type ActiveStatus = Extract<Status, "active" | "inactive">;
// 结果:"active" | "inactive"

type NonBanned = Exclude<Status, "banned">;
// 结果:"active" | "inactive"

Extract 是取交集,Exclude 是取差集。在处理复杂状态枚举时会用到。

六、条件类型与映射类型:进阶但值得了解

条件类型

ts 复制代码
type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

语法和三元运算符一样,但它运作在类型层面。很多 TS 内置的工具类型(比如 Exclude)底层就是用条件类型实现的。

映射类型

ts 复制代码
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

keyof T 拿到 T 的所有键,in 遍历它们,然后给每个键加上 readonly 修饰符。PartialRequiredReadonly 这些内置工具类型,背后全是映射类型。

七、interface vs type:别被这个问题困扰太久

这是 TS 社区里被讨论烂了的问题,结论其实挺简单:

interface 定义对象结构 ,因为它支持声明合并(Declaration Merging) ------同名 interface 会自动合并,这在扩展第三方库类型时很有用。

type 定义联合类型、交叉类型、别名 ,因为 interface 做不到 type Status = "active" | "inactive" 这种写法。

ts 复制代码
// interface:适合对象,支持 extends 和合并
interface User {
  name: string;
}
interface User {
  age: number; // 合并生效,不报错
}

// type:更灵活,适合联合/交叉/别名
type ID = string | number;
type AdminUser = User & { role: "admin" };

实际项目里,两者往往混用。不必教条,根据场景选最合适的

八、总结表

以下 12 个类型/特性,覆盖了日常前端开发 90% 以上的场景

类型 / 特性 核心价值
string / number / boolean 基础约束,防止类型误用
联合类型 处理多态数据,配合类型缩小使用
interface 定义数据结构,团队协作的契约
type 定义联合、交叉、别名,比 interface 更灵活
泛型 <T> 复用逻辑的同时保持类型安全
Record 快速定义映射/字典结构
Partial 更新接口的标配
Pick / Omit 从已有类型裁剪出你需要的形状
never 穷举检查,让编译器替你兜底

结语

TypeScript 的类型系统不是负担,是把 bug 消灭在编辑器里的机会。每一个精确的类型定义,都是在为未来的自己、为团队省下一次排查 bug 的时间。

从今天起,遇到 any,先想想能不能换成 unknown。遇到 string,先想想能不能换成字面量联合类型。把类型写得越具体,编译器能帮你做的就越多。

TypeScript 最好的使用方式,是把它当成一个不会累、不会忘、全年无休的代码审查员

相关推荐
AlienZHOU7 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc10 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师11 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen12 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒12 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow12 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81812 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007812 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明12 小时前
第 6 章 组件进阶
前端·vue.js
江华森12 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端