01 - TS 基础与类型系统
前置:JS 基础语法 至 JS 实战。TypeScript(下称 TS)是 JS 的超集:所有合法 JS 都是合法 TS,它额外提供静态类型系统。对 Java 程序员来说,TS 就是”给动态语言的灵活性装上编译器的护栏”。
1. TS 定位与价值
1.1 超集与编译擦除
TS 由微软开发,编译产物是普通 JS。类型标注在编译后被完全擦除(type erasure),运行时不存在任何类型检查代码:
// 源码 hello.ts
function greet(name: string): string {
return `你好, ${name}`;
}
// 编译产物 hello.js —— 类型信息全部消失
// function greet(name) {
// return `你好, ${name}`;
// }这意味着两点:
- 零运行时开销:类型只存在于开发期
- 不能依赖类型做运行时判断:
if (typeof x === "User")这种事不存在,类型不是值
1.2 为什么大型项目必须用 TS
三个核心收益,每个都直击纯 JS 的痛点:
| 收益 | 对应 JS 痛点 | Java 类比 |
|---|---|---|
| 重构信心 | 改函数签名后调用处静默出错 | IDE 全局重构 + 编译期检查 |
| IDE 补全 | 对象有什么字段全靠记忆 | 强类型的自动补全体验 |
| 契约文档 | 类型即文档,永不过期 | 接口定义就是团队契约 |
// 纯 JS 的经典事故:
function calcTotal(price, count) {
return price * count;
}
calcTotal("100", 5); // 返回 NaN,运行到这行才炸,且可能炸在千里之外// TS 把事故拦截在编辑器里:
function calcTotal(price: number, count: number): number {
return price * count;
}
calcTotal("100", 5);
// 编译错误:Argument of type 'string' is not assignable to parameter of type 'number'2. 安装与编译
2.1 命令行方式
npm install -g typescript # 全局安装 tsc 编译器
tsc --version # 确认安装成功
tsc hello.ts # 编译出 hello.js
tsc hello.ts --outDir dist # 指定输出目录
tsc --watch # 监听模式,保存即重编译2.2 tsconfig.json 初步
真实项目不逐个文件敲命令,而是在项目根放一个 tsconfig.json,之后直接 tsc 即可:
{
"compilerOptions": {
"target": "ES2020", // 编译成哪个版本的 JS
"module": "ESNext", // 模块规范
"outDir": "dist", // 产物目录
"strict": true, // 开启全部严格检查(强烈推荐)
"noImplicitAny": true, // 严格模式的组成部分:禁止隐式 any
"strictNullChecks": true // null/undefined 不能赋给其他类型
},
"include": ["src"]
}strict: true 是唯一推荐的姿势。不开严格的 TS 只剩半杯水;本教程所有示例均假定严格模式。
常用配置速查(排查工程问题时按图索骥):
| 选项 | 作用 |
|---|---|
| target | 决定编译产物语法等级,浏览器项目常用 ES2020 |
| strictNullChecks | null/undefined 不再可赋给一切类型 |
| noImplicitAny | 推断不出类型时直接报错而非默认 any |
| esModuleInterop | 兼容 CommonJS 模块的互操作细节 |
| skipLibCheck | 跳过 node_modules 内部类型检查,加快编译 |
实际前端工程通常不用手写 tsconfig——Vite、Vue CLI 等脚手架会生成并接管编译(见 TS 实战),但理解配置项含义在排查问题时至关重要。
3. 基础类型标注
3.1 原始类型
let age: number = 25; // number 统一整数与小数(对应 Java 的 double+long 合体)
let name: string = "张三";
let ok: boolean = true;
let nothing: null = null;
let notSet: undefined = undefined;
// 大整数与唯一符号
let big: bigint = 9007199254740993n;
let key: symbol = Symbol("k");对照 Java:TS 的原始类型标注几乎可以一一映射到 Java 包装类型,只是写在变量名后面且小写。
3.2 数组与元组
const nums: number[] = [1, 2, 3]; // 泛型写法 Array<number> 也合法
const names: Array<string> = ["a", "b"];
// 元组:固定长度、各位置类型不同的数组 —— Java 里没有直接对应物
const point: [number, number] = [116.4, 39.9];
const kv: [string, number] = ["age", 20];元组适合表达”位置即语义”的短序列(坐标、键值对);超过三四个元素建议改用对象接口,可读性更好。
// 元组的典型应用:Object.entries 的返回类型
const user = { name: "张三", age: 20 };
const entries: [string, unknown][] = Object.entries(user);
// 每一项都是 [键名, 值] 的定长结构
// 解构元组时每个位置都有精确类型
const [x, y] = point; // x: number, y: number —— 不是笼统的 number[]3.3 枚举 enum
enum Direction {
Up,
Down,
Left,
Right, // 默认从 0 自增:Up=0, Down=1...
}
enum Status {
Idle = "idle",
Loading = "loading",
Success = "success",
}
let dir: Direction = Direction.Up;
console.log(Status.Loading); // "loading"
console.log(Direction[1]); // "Down",数字枚举支持反向映射Java 类比:TS 枚举像简化版 java enum,但没有构造器和方法。实践中更常用字面量联合类型替代枚举(见下一章),因为枚举会生成真实的运行时对象,而联合类型完全擦除、零成本。
3.4 any vs unknown:重点区别
两者都表示”任意类型”,但安全性天差地别:
let a: any = getData();
a.foo.bar; // 不报错!any 会传染,等于关掉了类型检查
a(); // 不报错,运行时才炸
let u: unknown = getData();
u.toFixed; // 编译错误:使用前必须先收窄类型
if (typeof u === "number") {
u.toFixed(2); // 收窄后才能安全使用
}| 维度 | any | unknown |
|---|---|---|
| 可赋给任意类型 | 是 | 否(需收窄) |
| 类型检查 | 关闭 | 保留 |
| 定位 | 兼容旧代码的逃生舱 | 安全版的 any |
规范:any 仅用于渐进迁移的临时豁免,新代码一律 unknown。每出现一处 any,TS 的价值就被打一次折扣。
3.5 void 与 never
// void:函数无返回值(近似 Java 的 void)
function log(msg: string): void {
console.log(msg);
}
// never:永不返回(抛异常或死循环),也用于穷尽性检查(下一章)
function fail(): never {
throw new Error("终将失败");
}
function infiniteLoop(): never {
while (true) {}
}never 与 void 的区别:void 表示”返回 undefined”,never 表示”根本不会正常返回”。null/undefined 在 strictNullChecks 下只能赋给自身类型或 void——这条规则消灭了 Java 世界著名的 NullPointerException 一大类来源(Java 后来才用 Optional 补救)。
4. 函数类型
// 参数与返回值标注
function add(a: number, b: number): number {
return a + b;
}
// 可选参数:与默认参数共存,可选参必须在必选参后面
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}
greet("张三"); // 合法
greet("张三", "老师"); // 合法
greet("张三", undefined); // 也合法,触发可选语义
// 函数表达式 / 回调的类型
const compute: (a: number, b: number) => number = add;
// 高阶函数:把回调类型写进签名
function mapNums(arr: number[], fn: (x: number) => number): number[] {
return arr.map(fn);
}
mapNums([1, 2], x => x * 2); // [2, 4]4.1 重载简介
TS 支持声明式重载:多个签名 + 一个实现,实现体内部自行分派(没有 Java 那种真正的多份方法体):
function parse(input: string): string[];
function parse(input: number): number;
function parse(input: string | number): string[] | number {
if (typeof input === "string") return input.split(",");
return input * 10;
}
parse("a,b"); // string[]
parse(42); // number经验:重载声明维护成本高,多数场景用联合参数或泛型更简洁,仅在 API 形态差异大时使用。
4.2 this 参数与 void 返回的回调
两个容易踩的小细节:
// 回调返回值标注为 void 时,实现可以返回任何值(调用方约定忽略返回值)
arr.forEach((item): void => {
results.push(item); // push 有返回值也没问题
});
// 对象方法里担心调用方抽离方法导致 this 丢失?显式声明 this 类型
interface Counter {
count: number;
inc(this: Counter): number;
}
const c: Counter = { count: 0, inc() { return ++this.count; } };5. 类型推断:能省则省
TS 的类型推断能力很强,变量初始化、函数返回值等场景无需手写标注:
let count = 0; // 自动推断为 number
count = "x"; // 编译错误
const names = ["a", "b"]; // string[]
const total = names.length + 1; // number
function double(x: number) {
return x * 2; // 返回值自动推断为 number
}标注原则一句话:入参必须标(推断不了意图),出参交给推断,公共 API 才显式标返回值。到处写 : number 不是严谨,是噪音——这一点与 Java 必须处处声明类型形成鲜明对比,也是 TS 代码看起来比 Java 简洁的主要原因。
6. interface vs type
两种定义”形状”的方式:
// interface:接口
interface User {
id: number;
name: string;
readonly createdAt: Date; // readonly:创建后不可改
bio?: string; // ?:可选属性,bio?: string 等价于 bio: string | undefined
greet(): string;
}
// type:类型别名
type ID = number | string; // 别名的强项:联合、原始类型都能起名
type Point = { x: number; y: number };选择建议:
| 场景 | 用谁 |
|---|---|
| 描述对象/类的形状 | 都行,团队统一即可 |
| 需要 extends/implements 且关注可扩展性 | interface |
| 联合、元组、工具类型组合 | type(唯一选择) |
| 第三方库类型声明补全 | interface(声明合并特性) |
两者在对象层面高度重叠,社区主流是”对象用 interface,其余用 type”,坚持一致性比站队更重要。
7. 结构化类型 vs 名义化类型
这是 TS 与 Java 类型系统最深刻的差异,值得单独一节。
Java 是名义化(nominal)类型:两个类即使字段完全相同,只要名字不同就不能互相赋值。类型身份由”名字”决定。
TS 是结构化(structural)类型:只看形状不看名字——形状兼容即可互赋,本质是静态化的 duck typing。
interface Point2D { x: number; y: number }
interface Coordinates { x: number; y: number }
declare const p: Point2D;
const c: Coordinates = p; // 合法!形状相同,虽然名字不同同样的代码在 Java 中无法通过编译(除非其中一方继承另一方)。再体会一个体现”结构化更严格一面”的例子:
interface User { id: number }
interface Admin { id: number; permissions: string[] }
declare const admin: Admin;
const user: User = admin; // 合法:Admin 的形状包含 User 要求的全部
const admin2: Admin = {} as User; // 反方向不行:缺 permissions 字段结构化的推论还有”多余属性检查”:对象字面量直接赋值时会检查多余字段(避免拼错属性名),但经由变量的赋值不做此检查。理解了这一节,你就理解了为什么 TS 的 interface 能无缝对接 JSON 数据——后端返回的对象只要形状对,不需要任何适配层。
完整对比表:
| 维度 | Java | TypeScript |
|---|---|---|
| 判定依据 | 名字(继承体系) | 形状(成员集合) |
| 同形异名类型 | 不兼容 | 兼容 |
| duck typing | 无 | 静态化实现 |
| 泛型擦除 | 编译后擦除 | 编译后擦除(这点相同) |
| 空安全 | 默认可空,靠 Optional | strictNullChecks 下非空 |
8. 本章小结
- TS 是带静态类型的 JS 超集,类型编译期擦除,运行时零开销。
- 价值三件套:重构信心、IDE 补全、类型即文档。
strict: true是标配;any 是逃生舱,unknown 是安全替代。- 元组表达定长异构序列;枚举常被字面量联合替代。
- 标注原则:入参必须标、出参靠推断。
- interface 与 type 双轨制,对象用前者、联合用后者,团队一致优先。
- 结构化类型是 TS 与 Java 最深的鸿沟:看形状不看名字。
下一步深入类型体操的实用部分:TS 进阶类型。