给 JS穿上铠甲:TypeScript 基础核心概念详解(类型/接口/泛型)

1 阅读8分钟

前言

曾经,我沉醉于 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 中的 booleannumber

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;

但断言并非万能,盲目断言可能导致运行时错误。更优雅的方式是使用类型守卫。通过 typeofinstanceof 或自定义判断函数,在代码块内部收窄类型范围。这就像是在迷雾中点亮一盏灯,只有走进灯光范围(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 的惊吓。