TypeScript 完全指南(上):从零开始掌握类型系统

为什么说"任何 JavaScript 开发者都应该学 TypeScript"?读完前三部分你就明白了。

很多初学者觉得 TypeScript 只是给 JS 加了个":string"的语法糖,但实际上它带来的收益远超你的想象。本文将带你从为什么需要 TS ,到环境搭建 ,再到基础类型系统 ,一步步建立起坚实的 TS 知识体系。本文不涉及任何前端框架(React/Vue 等),所有示例纯 TypeScript,可独立运行。

第一部分:为什么你需要 TypeScript?

1.1 一个"血泪史"案例引入

想象你在开发一个电商网站的促销模块,有一个函数用来计算订单总价:

javascript

复制代码
// 这是纯 JavaScript
function calculateTotal(price, quantity, discount) {
    return price * quantity - discount;
}

由于 JS 是动态类型,你可能会在运行时遇到各种诡异情况:

javascript

复制代码
calculateTotal("100", 2, 20);  // 输出 "100200" - 20 → "10020020"? 实际:"100200" - 20 → NaN
calculateTotal(100, "2", 20);  // 输出 100 * "2" - 20 → 200 - 20 = 180(字符串 "2" 被隐式转换为数字)
calculateTotal(100, 2, "20");  // 输出 200 - "20" → 180("20" 被隐式转换为数字)

这些错误只有在代码真正运行到那一步时才会暴露。如果测试覆盖不充分,它们就会悄悄溜到生产环境,变成用户的差评。

这就是 TypeScript 要解决的核心问题:在编码阶段就捕获类型错误。

1.2 TypeScript 是什么?

TypeScript 由微软在 2012 年推出,是 JavaScript 的静态类型超集。用公式表示就是:

TypeScript = JavaScript + 类型系统 + 编译时检查

  • 超集 :所有合法的 JS 代码,在 TS 中也是完全合法的。你可以把 .js 文件直接改名为 .ts,它就能被 TS 编译器接受(当然类型检查可能会报警告)。

  • 静态类型:类型在编译时就确定,而不是等到运行时。

  • 编译时检查:在代码变成 JS 之前,TS 编译器会扫描所有类型不一致的地方并报错。

1.3 TypeScript 带来的核心价值

场景 纯 JavaScript 加上 TypeScript
函数参数传错类型 运行时产生奇怪结果,可能几天后才被发现 编写时 IDE 立即红线报错,编译不通过
访问对象不存在的属性 undefined,后续逻辑可能崩溃 类型错误,无法编译
重构(修改函数签名) 全局搜索,手动修改,容易遗漏 类型系统追踪所有调用处,一键重构
团队协作 需要大量文档和口头沟通 类型即文档,新人看类型就能理解用法
代码补全 基础的字符串匹配 精确的属性/方法提示,甚至能推导出返回类型

根据微软的统计数据,启用严格类型检查的 TypeScript 项目,运行时类型相关 bug 减少约 80%

1.4 一个直观的对比

typescript

复制代码
// 用 TypeScript 重写上面的 calculateTotal
function calculateTotal(price: number, quantity: number, discount: number): number {
    return price * quantity - discount;
}

// 下面任何一行都会导致编译错误,而不是运行时崩溃
// calculateTotal("100", 2, 20);  // ❌ 类型"string"的参数不能赋给类型"number"的参数
// calculateTotal(100, "2", 20);  // ❌ 同上
// calculateTotal(100, 2, "20");  // ❌ 同上
// calculateTotal(100, 2);        // ❌ 参数不足

// 正确调用
const total = calculateTotal(100, 2, 20);  // ✅ 类型安全,total 自动被推断为 number
console.log(total);  // 180

现在,任何试图传错类型的调用都会在编译时被拦截,你不需要运行代码就能发现问题。

第二部分:TypeScript 环境搭建(5分钟上手)

2.1 安装 TypeScript 编译器

TypeScript 编译器 tsc 是一个 npm 包,你可以全局安装,也可以在项目中本地安装。

bash

复制代码
# 全局安装(适合快速尝试)
npm install -g typescript

# 验证安装成功
tsc --version
# 输出类似:Version 5.8.2

# 本地安装(推荐用于实际项目)
mkdir my-ts-project && cd my-ts-project
npm init -y
npm install -D typescript

2.2 创建你的第一个 TypeScript 文件

新建一个 hello.ts

typescript

复制代码
// hello.ts
function greet(name: string): string {
    return `Hello, ${name}! Welcome to TypeScript.`;
}

const message = greet("TypeScript Beginner");
console.log(message);

2.3 编译并运行

bash

复制代码
# 使用 tsc 命令编译(如果全局安装)
tsc hello.ts

# 或者使用 npx(如果本地安装)
npx tsc hello.ts

# 上述命令会生成一个 hello.js 文件
# 然后用 Node 运行它
node hello.js
# 输出:Hello, TypeScript Beginner! Welcome to TypeScript.

2.4 初始化 tsconfig.json(推荐)

对于稍大一点的项目,你通常会需要一个配置文件来控制编译行为。

bash

复制代码
tsc --init

这会在当前目录生成一个 tsconfig.json。它包含了大量可配置选项,但现在你只需要知道:有了它,你就可以直接运行 tsc 而不指定文件名,编译器会按照配置处理所有 .ts 文件。

最简单的配置示例:

json

复制代码
{
  "compilerOptions": {
    "target": "ES2020",          // 编译到哪个版本的 JS
    "module": "commonjs",        // 模块系统
    "outDir": "./dist",          // 输出目录
    "rootDir": "./src",          // 源码目录
    "strict": true               // 开启所有严格类型检查(强烈推荐)
  }
}

然后在项目中创建 src 文件夹,把 .ts 文件放进去,运行 tsc 即可。

2.5 使用 ts-node 直接运行(开发阶段)

如果你不想每次都编译再运行,可以使用 ts-node

bash

复制代码
npm install -D ts-node
npx ts-node hello.ts

它会直接执行 .ts 文件,适合快速验证代码。

第三部分:TypeScript 基础类型系统(全文重点)

TypeScript 继承了 JS 的所有数据类型,并额外增加了类型注解更多类型。下面逐一讲解。

3.1 八大基础类型

3.1.1 布尔类型 boolean

typescript

复制代码
let isCompleted: boolean = false;
let isActive: boolean = true;
3.1.2 数字类型 number

包括整数、浮点数、十六进制、二进制、八进制等。

typescript

复制代码
let decimal: number = 42;
let hex: number = 0xf00d;      // 十六进制
let binary: number = 0b1010;    // 二进制
let octal: number = 0o744;      // 八进制
let float: number = 3.14159;
3.1.3 字符串类型 string

支持单引号、双引号和模板字符串。

typescript

复制代码
let single: string = 'Hello';
let double: string = "World";
let template: string = `Hello ${single} ${double}!`;
3.1.4 数组类型 Array<T>T[]

typescript

复制代码
// 两种等价写法
let numbers1: number[] = [1, 2, 3, 4];
let numbers2: Array<number> = [1, 2, 3, 4];

// 字符串数组
let fruits: string[] = ["apple", "banana", "orange"];

// 混合类型数组(联合类型)
let mixed: (string | number)[] = [1, "two", 3, "four"];
3.1.5 元组类型 Tuple

元组是固定长度、固定类型顺序的数组。

typescript

复制代码
// 定义:第一个元素是 string,第二个是 number,第三个是 boolean
let person: [string, number, boolean] = ["Alice", 25, true];

// 错误示例
// person = [25, "Alice", true];   // ❌ 类型顺序错误
// person = ["Alice", 25];         // ❌ 缺少第三个元素
// person = ["Alice", 25, true, "extra"]; // ❌ 超出长度

// 访问元素
console.log(person[0]);  // "Alice"
console.log(person[1]);  // 25

元组常用于表示结构化数据,比如 CSV 的一行、坐标等。

3.1.6 枚举类型 enum

枚举为一组数值或字符串常量赋予有意义的名称。

数字枚举(默认从 0 开始):

typescript

复制代码
enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right    // 3
}
let move: Direction = Direction.Up;
console.log(move);  // 0

// 手动指定值
enum HttpStatus {
    OK = 200,
    NotFound = 404,
    InternalError = 500
}
console.log(HttpStatus.OK);           // 200
console.log(HttpStatus[404]);         // "NotFound"(反向映射)

字符串枚举(更易读):

typescript

复制代码
enum LogLevel {
    INFO = "INFO",
    WARN = "WARN",
    ERROR = "ERROR"
}
let level: LogLevel = LogLevel.INFO;
console.log(level);  // "INFO"

提示:如果不需要反向映射,推荐使用字符串枚举或常量枚举(const enum),编译后内联,性能更好。

3.1.7 any 类型

any 表示关闭该变量的类型检查 ,可以赋任何值,可以调用任何方法。仅在万不得已时使用

typescript

复制代码
let loose: any = 42;
loose = "now a string";   // ✅
loose = true;             // ✅
loose.trim();             // ✅ 编译通过,但若此时 loose 是数字,运行时会崩溃

⚠️ 使用 any 会让 TypeScript 的保护失效。一般只有以下情况才考虑:迁移旧 JS 项目、与第三方非类型化库交互、或者你明确知道某个值类型复杂到无法描述(但此时往往可以用 unknown 代替)。

3.1.8 void 类型

void 通常用于函数没有返回值。

typescript

复制代码
function logMessage(msg: string): void {
    console.log(msg);
    // 没有 return,或者 return undefined;
}

let unusable: void = undefined;  // 声明 void 变量意义不大,只能赋值 undefined 或 null(如果 strictNullChecks 关闭)
3.1.9 nullundefined

在 TypeScript 中,nullundefined 既是值,也是类型。

typescript

复制代码
let n: null = null;
let u: undefined = undefined;

// 但在默认情况下(strictNullChecks: false),null 和 undefined 可以赋值给任何类型。
// 强烈建议开启 strictNullChecks,这样只有明确标记了 | null 或 | undefined 的变量才能接收它们。
3.1.10 never 类型

never 表示永远不会出现的值。它通常出现在两种场景:

  • 函数抛出异常,永不返回

  • 函数进入无限循环

typescript

复制代码
// 抛出错误的函数,返回类型是 never
function throwError(message: string): never {
    throw new Error(message);
}

// 无限循环
function infiniteLoop(): never {
    while (true) {
        console.log("running");
    }
}

never 还有一个重要用途:穷尽性检查(后面高级部分会详讲)。

3.1.11 object 类型

object 表示非原始类型(即不是 numberstringbooleansymbolnullundefined 的值)。

typescript

复制代码
let obj: object = { name: "Alice" };
obj = [1, 2, 3];      // ✅ 数组也是对象
obj = () => {};       // ✅ 函数也是对象
// obj = 42;          // ❌ 原始类型不行

// 更常见的做法是使用接口来描述对象的具体形状(后面会讲)

3.2 类型推断

TypeScript 会根据你的赋值自动推断类型,你不需要为所有变量都加上注解。

typescript

复制代码
let message = "Hello TS";   // 推断为 string
let count = 10;             // 推断为 number
let flags = [true, false];  // 推断为 boolean[]

// 尝试重新赋值不兼容的类型会报错
// message = 42;            // ❌ 不能将 number 赋给 string
// flags.push("string");    // ❌ 不能将 string 赋给 boolean

类型推断在大多数情况下已经足够,但在变量声明时没有赋值,或者需要明确的类型约束时,才需要手动注解。

3.3 联合类型

联合类型用 | 表示,表示一个值可以是几种类型之一。

typescript

复制代码
// 一个变量可以是数字或字符串
let id: number | string;
id = 123;      // ✅
id = "ABC456"; // ✅
// id = true;  // ❌

// 函数参数使用联合类型
function printId(id: number | string): void {
    console.log(`ID is: ${id}`);
}
printId(101);
printId("user-202");
3.3.1 类型守卫(收窄联合类型)

当你需要在函数内部区分联合类型的实际类型时,可以使用 typeofinstanceof 或自定义类型守卫。

typescript

复制代码
function processValue(value: number | string): string {
    if (typeof value === "string") {
        // 在这个分支内,TypeScript 知道 value 是 string
        return value.toUpperCase();
    } else {
        // 这里 value 被收窄为 number
        return value.toFixed(2);
    }
}

console.log(processValue("hello"));  // "HELLO"
console.log(processValue(3.1415));   // "3.14"

3.4 unknown 类型(比 any 安全得多)

unknown 可以理解为类型安全的 any。它也可以接受任何值,但在使用它之前,你必须先进行类型检查。

typescript

复制代码
let data: unknown = fetchData();  // fetchData 返回 unknown

// 下面的操作都不被允许,因为 unknown 未经检查
// data.trim();       // ❌ 类型 "unknown" 上不存在属性 "trim"
// data.toFixed(2);   // ❌

// 必须先收窄类型
if (typeof data === "string") {
    data.trim();      // ✅ 此时 data 是 string
} else if (typeof data === "number") {
    data.toFixed(2);  // ✅ 此时 data 是 number
}

最佳实践 :对于来自外部 API 或用户输入的数据,永远使用 unknown,而不是 any

3.5 类型别名(Type Aliases)

类型别名可以给一个类型起一个新名字,方便复用。

typescript

复制代码
// 联合类型别名
type ID = number | string;
type Status = "pending" | "success" | "error";

// 函数类型别名
type Callback = (result: string) => void;

// 使用别名
let userId: ID = "user123";
let taskStatus: Status = "pending";
let handler: Callback = (res) => console.log(res);

类型别名非常灵活,可以描述任何类型,包括元组、联合、交叉等。

3.6 可选属性和只读属性(针对对象类型)

虽然对象类型的完整介绍在后面接口部分,但这里先简单提一下基础用法:

typescript

复制代码
// 使用类型别名定义对象形状
type Person = {
    name: string;
    age: number;
    email?: string;      // 可选属性
    readonly id: number; // 只读属性
};

const alice: Person = {
    name: "Alice",
    age: 25,
    id: 1001
    // email 可以省略
};
// alice.id = 1002;     // ❌ 只读属性不能修改

3.7 类型断言

当你比 TypeScript 编译器更了解某个值的类型时,可以使用类型断言告诉编译器"相信我,它就是那个类型"。

typescript

复制代码
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;

// 尖括号语法(在 .tsx 文件中不能用)
let strLength2: number = (<string>someValue).length;

注意:类型断言不会改变运行时的值,它只在编译时影响类型检查。如果你断言错误,程序仍可能运行时崩溃。

3.8 字面量类型

TypeScript 支持将字符串、数字、布尔值本身作为类型。这常与联合类型配合使用,实现类似枚举的效果。

typescript

复制代码
// 字符串字面量类型
type Direction = "up" | "down" | "left" | "right";
let move: Direction = "up";   // ✅
// move = "north";            // ❌

// 数字字面量类型
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
let roll: Dice = 4;           // ✅
// roll = 7;                  // ❌

// 布尔字面量类型
let isTrue: true = true;      // 只能赋值为 true(几乎无用,但理解概念)

3.9 类型系统小结

现在你应该已经掌握了 TypeScript 的基础类型系统。下表总结了常用类型及其用途:

类型 示例 说明
boolean true/false 布尔值
number 42, 3.14, 0xf00d 所有数字
string "hi", 'hi', \hi`` 文本
Array<T> number[] 元素类型相同的列表
[T, U] [string, number] 固定长度、固定顺序的元组
enum enum Color { Red, Green } 命名常量集合
any any 任意值(逃逸类型检查)
void void 无返回值
never never 永不存在的值
unknown unknown 任意值(需要类型检查后使用)
` ` `string
type `type ID = string number`

掌握这些基础类型,你已经能够为大多数 JavaScript 代码添加准确的类型注解了。下一步(后续文章)我们将深入函数、接口、类、泛型等内容。

写在最后

本文是 TypeScript 完整指南的上篇,涵盖了为什么需要 TS环境搭建 以及最核心的基础类型系统。中篇我们将讨论函数类型、接口与类,下篇则会深入泛型与高级类型。

现在,你可以动手在自己的项目里尝试用 TypeScript 编写几行代码,感受一下类型检查带来的安全感。如果你坚持写完全文的所有示例,相信你已经超越了 80% 的 TypeScript 初学者。

📌 下篇预告:函数类型、可选参数、重载;接口与类;高级类型(交叉类型、索引类型等)。敬请期待!

本回答由 AI 生成,内容仅供参考,请仔细甄别。

相关推荐
乘风gg12 分钟前
还在养虾吗?虾王已诞生:微信龙虾 ClawBot
前端·ai编程·claude
小小小小宇27 分钟前
LLM 长期记忆构建
前端
lichenyang45340 分钟前
从 Express 老项目到 NestJS + Docker:一次车辆管理系统的渐进式重构
前端
Momo__2 小时前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富2 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇2 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇2 小时前
React中的forwardRef
前端·react.js·面试
Flynt2 小时前
装上TypeScript 7.0 RC之后,最让我意外不是10倍提速
typescript·visual studio code
疯狂SQL2 小时前
手写高性能在线 JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战
typescript·json·next.js·web worker·前端性能优化·esbuild·源码实战
槑有老呆2 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端