前言
曾经,我沉醉于 JavaScript 的灵活与自由。变量可以随意赋值,函数参数无需声明,一切看起来都那么随心所欲。直到有一天,一个看似简单的 undefined is not a function 错误在生产环境爆发,我才惊觉:这种“自由”有时更像是在没有护栏的悬崖边跳舞。
在深入探索 TypeScript 的过程中,我深刻体会到了它带来的秩序之美。今天,我想结合实战代码,和大家聊聊 TypeScript 的基础,希望能帮同样在转型的你,穿上铠甲,从容前行。
一、混沌与秩序:为什么我们需要类型?
在学习 TypeScript 之前,我们先回看一下 JavaScript 的世界。
在 JavaScript 中,变量就像是一个个没有标签的盒子。你可以把数字放进去,下一秒又可以把它拿出来,换成一个字符串。这种“弱类型”特性虽然开发速度快,但也埋下了隐患。编译器直到代码运行的最后一刻,才知道盒子里装的是什么。如果此时盒子里的东西不是我们预期的,程序就会崩溃。
// JavaScript 的动态类型陷阱
function add(a, b) {
// 运行时才能发现类型问题
if (typeof a === 'number' && typeof b === 'number') {
return a + b
}
return undefined;
}
// 调用时传入了字符串,编译器不会报错,但逻辑可能非预期
const result = add(1, '2');
console.log(result); // 输出 undefined,而非报错
相比之下,C 语言等“强类型”语言则要求我们在定义变量时就必须声明类型,一旦类型不匹配,编译直接失败。
TypeScript 正是为了解决 JavaScript 的痛点而生。它给 JavaScript 加上了静态类型的“护栏”。它不改变 JS 的运行机制,而是在代码运行前(编译阶段)就帮我们检查类型是否正确。
看,这是迈向 TypeScript 的第一步:
// TypeScript 的类型注解
let a: number = 1;
// a = '2'; // ❌ 报错!TypeScript 会大声告诉你:'2' 不能赋值给 number 类型的变量
console.log(a);
这就好比给变量贴上了标签。一旦贴上 number 的标签,这个盒子就只能装数字。如果你试图塞进字符串,TS 编译器会立即拦截,将错误扼杀在摇篮里。
二、基础数据类型:构建类型的基石
有了类型注解的概念,我们就可以开始构建更复杂的数据结构了。TS 提供的一系列基础类型,是我们搭建程序的砖瓦。
1. 布尔值与数字
最基础的类型,对应 JS 中的 boolean 和 number。
let isDone: boolean = false;
let count: number = 123;
2. 字符串与字面量类型
除了普通的字符串,TS 还允许我们定义“字面量类型”,即变量只能是某个特定的字符串值。这在做状态管理时非常有用,就像给变量限定了唯一的“身份证号”。
const hello = 'hello';
const a: 'hello' = 'hello'; // ✅ 正确
// const b: 'hello' = 'world'; // ❌ 错误,只能是 'hello'
3. 数组与元组
数组用来存储相同类型的列表,而元组(Tuple)则像是固定长度的“混合容器”,可以存储不同类型的值,但顺序和类型必须严格对应。
// 普通数组:只能装数字
let list: number[] = [1, 2, 3];
// 元组:第一个必须是 number,第二个必须是 string
let tuple: [number, string] = [1, 'hello'];
// let errorTuple: [number, string] = ['hello', 1]; // ❌ 类型错位,编译器直接红牌罚下
4. 枚举(Enum)
枚举让我们可以定义一组命名的常量,让代码可读性更强。就像给方向定义了名字,而不是使用晦涩的数字。
enum Direction {
NORTH,
SOUTH,
EAST,
WEST
}
let dir: Direction = Direction.NORTH; // 比直接写 0 更易读,代码自文档化
5. Any 与 Unknown:双刃剑与保险丝
在迁移旧代码时,我们难免会遇到类型不确定的情况。JS 开发者习惯用 any,它意味着“关闭类型检查”。
let notSure: any = 100;
notSure = '123'; // ✅ 随便改,TS 不管了,这里失去了保护
但 any 用多了,TS 就退化成 JS 了,失去了保护意义。
TS 提供了更安全的 unknown。它和 any 一样可以接收任何类型,但在你使用它之前,必须进行类型判断或断言。这就像是一个带保险丝的电路,虽然通电,但必须先确认安全才能使用。
let value: unknown = 123;
value = '123';
// let abc: string = value; // ❌ 报错!不能直接把 unknown 赋给 string
// 必须先收窄类型,确认安全
if (typeof value === 'string') {
let abc: string = value; // ✅ 安全了,TS 知道此时 value 一定是 string
}
6. Void, Null, Undefined 与 Symbol
这些类型分别对应无返回值、空值、未定义以及唯一的标识符。特别是 void,常用于没有返回值的函数,明确告诉调用者“别指望我有返回值”。
function warnUser(): void {
console.log("This is my warning message");
// 这里不需要 return 任何值,甚至 return undefined 也是允许的
}
三、对象与接口:描绘数据的形状
在实际开发中,我们处理最多的往往是对象。如何描述一个对象的“形状”?TS 提供了 接口 和 类型别名 。
接口就像是建筑的蓝图,规定了对象必须拥有哪些属性,哪些是可选的。
interface Person {
name: string;
age: number;
sex?: string; // ? 表示可选属性,就像装修时的“预留接口”
}
const p: Person = {
name: '探长',
age: 20
// sex 属性可选,不写也不会报错,系统依然认为它是合法的 Person
};
除了接口,TS 还提供了强大的类型运算。我们可以像搭积木一样组合类型。
使用了交叉类型(&)来合并两个类型,创造出新的形态:
type PartialX = { x: number };
// Point 类型既要有 x,也要有 y,通过 & 将两个类型“焊接”在一起
type Point = PartialX & { y: number };
const p: Point = {
x: 1,
y: 2
};
这就像是将两块拼图完美地拼在一起,形成了一个新的、更完整的形状。这种组合能力让 TS 在处理复杂数据结构时游刃有余,避免了重复定义。
四、泛型:类型的“模具”
如果说接口是描述具体对象的蓝图,那么泛型(Generics)就是制造蓝图的模具。
想象一下,你要写一个函数,它的功能是“原样返回传入的参数”。
- 如果传入数字,返回数字;
- 如果传入字符串,返回字符串。
在没有泛型之前,我们可能要用 any,但这会丢失类型信息,导致调用者不知道返回的是什么。泛型允许我们将类型作为一个参数传递进去,让函数具有“多态”的能力,且保持类型安全。
// T 是一个类型占位符,调用时确定具体是什么类型
// 就像是一个通用的容器,里面装什么,倒出来就是什么
function identity<T>(value: T): T {
return value;
}
// 调用时指定 T 为 number
const num = identity<number>(100);
// num 的类型被推断为 number
// 调用时指定 T 为 string
const str = identity<string>('hello');
// str 的类型被推断为 string
泛型还可以同时接受多个类型参数,甚至用于约束数组等复杂结构,极大地提高了代码的复用性:
// 定义一个既可以存 number 也可以存 string 的数组
let arr: Array<number | string> = [1, 2, 3, '1'];
泛型让代码变得更加灵活且安全,它是 TS 进阶的必经之路,也是区分新手与老手的关键标志。
五、类型断言与守卫:掌控不确定性
有时候,我们比编译器更清楚某个变量的类型。比如在处理 DOM 元素或者第三方库返回的数据时。这时,我们可以使用类型断言,告诉编译器:“相信我,我知道我在做什么。”
TS 提供了两种断言方式,推荐使用的是 as 语法:
let someValue: any = 'this is a apple';
// 方式一:as 语法(推荐,兼容性好)
let strLength = (someValue as string).length;
// 方式二:尖括号语法(不能在 JSX/TSX 中使用,容易与 HTML 标签混淆)
// let strLength = (<string>someValue).length;
但断言并非万能,盲目断言可能导致运行时错误。更优雅的方式是使用类型守卫。通过 typeof、instanceof 或自定义判断函数,在代码块内部收窄类型范围。这就像是在迷雾中点亮一盏灯,只有走进灯光范围(if 语句块内),变量的真实面目才会被看清,TS 也会随之放宽限制,允许你访问特定类型的方法。
function printId(id: number | string) {
if (typeof id === "string") {
// 在这里,id 的类型被收窄为 string
console.log(id.toUpperCase());
} else {
// 在这里,id 的类型被收窄为 number
console.log(id);
}
}
结语:从束缚到自由
回顾这段旅程,我们经历了从“随意赋值”的混乱,到“严格定义”的束缚,最后达到了“类型安全下的自由”。TypeScript 并不是要给 JavaScript 戴上沉重的枷锁,而是为我们提供了一套精密的导航系统。
学习之路漫长,这些基础只是探索 TS 世界的起点。希望这篇文章能帮你理清 TS 的脉络,让你在写代码时多一份底气,少一份 undefined 的惊吓。