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 生成,内容仅供参考,请仔细甄别。

相关推荐
之歆1 小时前
Day01_ES6+ 专业指南:从基础到实战的现代JavaScript开发(下)
前端·javascript·es6
lichenyang4531 小时前
鸿蒙 MVVM 实战:从 Demo 到工程化,聊聊登录、状态管理与埋点系统设计
前端
IT_陈寒2 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
kyriewen2 小时前
AI生成代码快如闪电,但我修了三个小时——它到底帮了谁?
前端·javascript·ai编程
ayqy贾杰3 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox3 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全
miaowmiaow3 小时前
PSD2Code 近期更新与深度解析:从设计稿到生产级代码的完整技术栈
前端·人工智能·ai编程
Hilaku3 小时前
多标签页并发请求导致 Token 刷新失败?只有 15行代码就能解决 !
前端·javascript·程序员
烛衔溟3 小时前
TypeScript 类的静态成员与静态方法
开发语言·javascript·typescript