TypeScript 简介(选读)

前面几篇我们一直在写 JavaScript。这篇是选读内容,来聊聊 TypeScript

如果你看过前端项目的源码,会发现文件后缀经常是 .ts 而不是 .js,函数参数后面还跟着冒号和类型名,这就是 TypeScript。

本篇的 TypeScript 代码不能在网页上直接运行,你可以用 TypeScript Playground 在线尝试,也可以按照后面「编译和运行」一节的说明在本地运行。

TypeScript 是什么

一句话概括:TypeScript 就是带类型的 JavaScript。

它是 JavaScript 的超集,所有合法的 JavaScript 代码都是合法的 TypeScript 代码,TypeScript 只是在此基础上加了一套静态类型系统,让你可以给变量、函数参数和返回值标注类型。

为什么需要类型?看个例子:

JavaScript 不检查类型,add("1", "2") 不会报错,而是默默做了字符串拼接。这种 bug 在小脚本里无所谓,但在大型项目中排查起来很痛苦。

TypeScript 的做法是在编译阶段就拦住这类错误:

function add(a: number, b: number): number {
    return a + b;
}

add(1, 2);        // 3,没问题
add("1", "2");    // 编译错误!参数类型不匹配

a: number 表示参数 a 必须是数字,括号后面的 : number 表示返回值也必须是数字。传入字符串时,TypeScript 编译器直接报错,不用等到运行时才发现。

换句话说,TypeScript 最终运行的还是 JavaScript,类型标注不会出现在最终代码里。TypeScript 编译器的作用就是在「TypeScript → JavaScript」这一步做类型检查,确保生成的 JavaScript 没有类型层面的意外问题,从而提高程序的可靠性。你可以把它理解成一个更严格的代码检查工具,帮你在写代码阶段就把潜在的 bug 揪出来。

基本类型标注

TypeScript 的类型标注语法很直观,在变量名或参数名后面加 : 类型 就行:

// 基本类型
let userName: string = "Alice";
let age: number = 25;
let isStudent: boolean = true;

// 数组
let scores: number[] = [90, 85, 92];
let names: string[] = ["Alice", "Bob"];

函数的参数和返回值也可以标注类型。加 ? 表示参数是可选的:

function greet(name: string, title?: string): string {
    if (title) {
        return "Hello, " + title + " " + name;
    }
    return "Hello, " + name;
}

greet("Alice");           // "Hello, Alice"
greet("Alice", "Prof.");  // "Hello, Prof. Alice"

TypeScript 还有类型推断能力,很多时候不用手动标注,编译器能自动推断:

let x = 10;          // 自动推断为 number
let s = "hello";     // 自动推断为 string
let arr = [1, 2, 3]; // 自动推断为 number[]

所以实际写 TypeScript 时,不需要给每个变量都加类型。编译器能推断的就让它推断,只在必要的地方(函数参数、复杂对象)手动标注就行。

接口和类型别名

当你需要描述一个对象的结构时,可以用 interface

interface User {
    name: string;
    age: number;
    email?: string;  // 可选属性
}

function printUser(user: User) {
    console.log(user.name + ", " + user.age + " 岁");
}

printUser({ name: "Alice", age: 25 });             // OK
printUser({ name: "Bob", age: 30, email: "..." });  // OK
printUser({ name: "Charlie" });                      // 错误!缺少 age

interface 就像是给对象画了个「模板」,规定了必须有哪些属性、每个属性是什么类型。email?: string 中的 ? 表示这个属性可以没有。

type 关键字可以做类似的事情,还能定义联合类型:

// 联合类型:ID 可以是字符串或数字
type ID = string | number;

function findUser(id: ID) {
    console.log("查找用户:" + id);
}

findUser(123);      // OK
findUser("abc");    // OK
findUser(true);     // 错误!

简单来说,描述对象结构用 interface,定义联合类型或起别名用 type

泛型基础

如果你学过 Java 或 C++ 的泛型,TypeScript 的泛型思路类似。没学过也没关系,看个例子就明白了。

假设你要写一个函数,返回数组的第一个元素:

// 写死类型,只能处理 number 数组
function firstNumber(arr: number[]): number {
    return arr[0];
}

// 用泛型,什么类型的数组都能处理
function first<T>(arr: T[]): T {
    return arr[0];
}

first([1, 2, 3]);          // 返回类型是 number
first(["a", "b", "c"]);    // 返回类型是 string

<T> 是一个类型参数,调用时 TypeScript 会根据传入的参数自动推断 T 是什么类型。你不用显式写 first<number>([1, 2, 3]),编译器能自己推断出来。

编译和运行

TypeScript 在运行前需要先编译成 JavaScript(把类型标注去掉),传统做法是用 tsc(TypeScript Compiler):

# npm 是 Node.js 自带的包管理器,-g 表示全局安装
npm install -g typescript

# 编译,在同目录下生成 hello.js
tsc hello.ts

# 运行编译后的 JavaScript
node hello.js

举个例子,下面这段 TypeScript 代码:

function add(a: number, b: number): number {
    return a + b;
}
console.log(add(1, 2));

编译后生成的 JavaScript:

function add(a, b) {
    return a + b;
}
console.log(add(1, 2));

所有类型标注都被移除了,剩下的就是普通的 JavaScript。TypeScript 的类型标注只在编译阶段起作用,运行时没有任何开销。

实际项目中,TypeScript 的编译配置写在项目根目录的 tsconfig.json 文件里:

{
    "compilerOptions": {
        "target": "ES2020",      // 编译目标:生成 ES2020 语法的 JavaScript
        "module": "commonjs",    // 模块系统:使用 CommonJS(Node.js 默认)
        "strict": true,          // 开启严格类型检查
        "outDir": "./dist"       // 编译输出目录
    },
    "include": ["src/**/*"]      // 要编译的源文件范围
}

strict: true 开启严格模式,能最大程度发挥类型检查的作用,新项目建议都开启。有了这个配置文件,在项目根目录运行 tsc 就会按配置编译所有文件。

小结

TypeScript 本质上就是给 JavaScript 加了一层类型保护网。写代码时编译器能直接指出类型错误,IDE 的自动补全也更精准,不用猜一个变量到底是什么类型。

如果你之前写过 Java、C++ 或 Go,TypeScript 的类型系统会让你觉得很亲切。如果你一直写的是 Python 或 JavaScript,需要一点适应时间,但带来的好处是实实在在的。

目前 React、Vue、Next.js 这些主流的前端开发框架都原生支持 TypeScript,新项目基本都直接上 TypeScript 了。

至此 JavaScript 基础系列就全部讲完了。掌握了这些内容,你已经具备了用 JavaScript 刷题和做项目的基础,如果在项目中遇到 .ts 文件也不用发怵了。