什么是 TypeScript?一篇入门教程 – wiki基地


什么是 TypeScript?一篇通俗易懂的入门指南

引言:拥抱现代前端开发的新利器

JavaScript 无疑是当今互联网世界最重要的编程语言之一。从简单的网页脚本到复杂的单页应用、服务器端应用(Node.js),甚至是移动应用(React Native),JavaScript 无处不在。然而,随着项目规模的不断扩大和团队协作的日益紧密,JavaScript 动态类型的特性也暴露出一些痛点:运行时错误频发、代码难以维护和重构、缺乏强大的工具支持等。

为了解决这些问题,微软开发并推出了 TypeScript。它迅速获得了开发者社区的广泛认可,并成为了许多大型项目和现代前端框架(如 Angular、Vue 3、React)的首选语言。

那么,TypeScript 究竟是什么?为什么它如此受欢迎?作为一名 JavaScript 开发者,又该如何踏上 TypeScript 之旅呢?本文将为你详细解答这些问题,并提供一份入门指南。

1. JavaScript 的痛点:动态类型的挑战

要理解 TypeScript 的价值,我们首先需要回顾一下纯 JavaScript 在开发过程中可能遇到的问题。JavaScript 是一种动态类型语言,这意味着变量的类型在运行时才能确定,并且可以随时改变。

例如:

“`javascript
let data = 100; // data 是一个数字
console.log(typeof data); // 输出: number

data = “hello”; // 现在 data 变成了字符串
console.log(typeof data); // 输出: string

data = { name: “Alice” }; // 现在 data 变成了对象
console.log(typeof data); // 输出: object
“`

这种灵活性带来了便捷,但也隐藏了风险:

  1. 运行时错误: 类型错误通常只会在代码执行时才被发现。比如,你可能调用了一个不属于当前类型变量的方法,直到运行到那一行代码,程序才会崩溃。
    “`javascript
    function greet(name) {
    // 假设我们期望 name 是一个字符串
    console.log(“Hello, ” + name.toUpperCase());
    }

    greet(“Alice”); // 正常输出: Hello, ALICE
    greet(123); // 运行时出错:TypeError: name.toUpperCase is not a function
    “`
    这种错误在大型复杂应用中尤为难以定位和调试。

  2. 代码难以理解和维护: 当你阅读一段别人的(或自己写于很久以前的)JavaScript 代码时,往往需要通过上下文推断变量的类型。如果代码逻辑复杂,这个过程会非常耗时且容易出错。
    javascript
    function processUserData(user) {
    // user 是什么类型?有哪些属性?每个属性是什么类型?
    // 如果没有文档或注释,很难确定
    if (user && user.address && user.address.city) {
    console.log("User lives in " + user.address.city);
    }
    // ... 更多处理 user 的代码
    }

    这降低了代码的可读性,增加了维护成本。

  3. 重构风险高: 修改代码时,你很难确定某个变量被用在了哪些地方,期望的类型是什么。一个小的改动可能在程序的某个角落引发类型错误,而且你无法在修改时立即发现。

  4. 工具支持受限: 缺乏明确的类型信息,导致代码编辑器(IDE)的智能提示(IntelliSense)和代码导航功能不够强大。你无法轻松地查看一个对象有哪些属性或一个函数需要什么参数,这影响了开发效率。

2. TypeScript:JavaScript 的超集与类型卫士

TypeScript 正是为了解决上述问题而诞生的。 简单来说,TypeScript 是 JavaScript 的一个超集(Superset)。这意味着:

  • 所有合法的 JavaScript 代码都是合法的 TypeScript 代码。 你可以直接将现有的 .js 文件后缀改为 .ts,然后在大多数情况下,它依然可以正常工作(当然,这样还没有发挥 TypeScript 的优势)。
  • TypeScript 在 JavaScript 的基础上添加了类型系统和其他特性。 你可以在代码中为变量、函数参数、返回值等指定类型。

TypeScript 代码不能直接在浏览器或 Node.js 环境中运行。它需要通过一个叫做 TypeScript 编译器(tsc) 的工具,将 .ts 文件编译(Compile)成纯 JavaScript 代码。

这个编译过程是 TypeScript 的核心所在:

  1. 静态类型检查: 在编译阶段(也就是代码运行之前),TypeScript 编译器会检查你的代码中是否存在类型错误。如果在 greet(123) 这样的地方,你期望 name 是字符串但却传入了数字,编译器会立即报错,而不是等到运行时才崩溃。
  2. 代码转换: 如果类型检查通过,编译器会将 TypeScript 特有的语法(比如类型注解)移除,并将一些新的语言特性(比如 ES6+ 语法)转换为目标版本的 JavaScript 代码(比如 ES5),以便在各种环境中运行。

总结: TypeScript 提供了一层额外的“类型安全网”,在开发阶段就能捕获潜在的类型错误,大大减少了运行时出错的可能性,提高了代码的健壮性。同时,明确的类型信息也极大地改善了代码的可读性、可维护性和工具支持。

3. TypeScript 的核心概念:类型系统初探

TypeScript 的魅力在于其强大的类型系统。下面介绍一些最核心、最基础的类型概念和语法。

3.1 基本类型(Basic Types)

TypeScript 支持 JavaScript 中所有的基本类型,并为它们提供了明确的类型名称:

  • number: 表示数字,包括整数和浮点数。
    typescript
    let age: number = 30;
    let price: number = 19.99;
    // age = "四十"; // 编译错误:不能将类型“string”分配给类型“number”。

  • string: 表示字符串。
    typescript
    let name: string = "Alice";
    let greeting: string = `Hello, ${name}!`;

  • boolean: 表示布尔值,truefalse
    typescript
    let isDone: boolean = false;
    let hasStarted: boolean = true;

  • array: 表示数组。有两种常用的声明方式:

    • 元素类型后跟 []
      typescript
      let numbers: number[] = [1, 2, 3];
      let names: string[] = ["Alice", "Bob"];
      // numbers.push("four"); // 编译错误:不能将类型“string”的参数分配给类型“number”。
    • 使用泛型数组 Array<元素类型>
      typescript
      let numbers2: Array<number> = [4, 5, 6];
  • object: 表示非原始类型,即除 number, string, boolean, symbol, null, undefined 之外的类型。通常用于描述对象结构,但单独使用 object 类型不够精确,我们通常会使用接口(Interface)或类型别名(Type Alias)来描述更具体的对象形状。

  • nullundefined: 它们各自是自己的类型,也可以作为其他类型的子类型(取决于配置)。
    typescript
    let n: null = null;
    let u: undefined = undefined;

    注意: 在严格模式下 (strictNullChecks: truetsconfig.json 中),nullundefined 不能赋值给其他类型的变量,除非你明确使用了联合类型(后面介绍)。

3.2 any 类型(Escape Hatch)

有时候,你可能不确定一个变量的类型,或者你正在处理来自外部的、结构不确定的数据。这时可以使用 any 类型。带有 any 类型的变量可以被赋予任意类型的值,并且你可以像对待任意类型一样访问它的属性或方法,而不会进行类型检查。

“`typescript
let dynamicValue: any = 10;
dynamicValue = “hello”;
dynamicValue = { foo: 123 };

dynamicValue.bar(); // 编译器不会报错,但运行时可能会出错
“`

警告: 使用 any 类型会绕过 TypeScript 的类型检查,失去了类型安全带来的好处。因此,应该尽量避免使用 any,除非万不得已。如果可能,尝试使用更具体的类型,或者利用联合类型、接口等来描述可能的类型范围。

3.3 函数类型(Function Types)

你可以为函数的参数和返回值指定类型,这极大地提高了函数使用的安全性。

“`typescript
// 为参数和返回值指定类型
function add(a: number, b: number): number {
return a + b;
}

// greet 函数接收一个 string 类型的参数,没有明确的返回值类型(默认为 void)
function greet(name: string): void {
console.log(“Hello, ” + name);
}

// greet(123); // 编译错误:类型“number”的参数不能赋给类型“string”的参数。

// 函数表达式也可以指定类型
const multiply = (x: number, y: number): number => {
return x * y;
};

// 也可以定义函数变量的类型(函数签名)
let calculation: (x: number, y: number) => number; // 定义一个变量,它必须是一个函数,接收两个 number 参数,返回一个 number
calculation = add; // OK
// calculation = greet; // 编译错误:类型不匹配
“`

3.4 接口(Interfaces)

接口是 TypeScript 中描述对象形状(Shape)的重要方式。它定义了一个对象应该包含哪些属性,以及这些属性的类型。

“`typescript
// 定义一个接口,描述 Person 对象的结构
interface Person {
name: string;
age: number;
isStudent?: boolean; // 属性名后加 ? 表示该属性是可选的
[propName: string]: any; // 允许存在任意数量的额外属性,这些属性名是字符串,值可以是任意类型
}

// 创建一个符合 Person 接口的对象
let user: Person = {
name: “Charlie”,
age: 25
};

// 另一个符合 Person 接口的对象,包含可选属性
let anotherUser: Person = {
name: “David”,
age: 30,
isStudent: true
};

// 不符合接口定义的对象会报错
// let invalidUser: Person = {
// name: “Eve” // 编译错误:缺少属性 “age”,但类型 “Person” 中需要该属性。
// };

// 通过接口类型访问属性,编辑器会提供智能提示
console.log(user.name); // 会有 name 和 age 的智能提示

// 接口也可以描述函数类型或类实现的契约,这里主要看对象形状的用法
“`

接口提高了代码的可读性,明确了数据结构,并且在团队协作时非常有用——大家可以遵循同一份接口定义。

3.5 类型别名(Type Aliases)

类型别名允许你为任何类型创建一个新的名字。这对于复杂类型(如联合类型、交叉类型)或重复使用的类型定义非常有用。

“`typescript
// 为字符串或数字创建别名
type ID = number | string;

let userId: ID = 101;
userId = “user-abc”;
// userId = true; // 编译错误

// 为对象类型创建别名(与接口类似,但 type 可以用于更多类型)
type Point = {
x: number;
y: number;
};

let origin: Point = { x: 0, y: 0 };

// 也可以为函数类型创建别名
type GreetingFunction = (name: string) => string;

const sayHello: GreetingFunction = (name) => Hello, ${name}!;
“`

接口(Interface)和类型别名(Type Alias)在很多情况下可以互换使用,用于描述对象形状。但它们之间也有一些技术差异,比如接口可以被继承和实现(用于类),而类型别名更灵活,可以用于联合类型、交叉类型、原始类型等。对于描述对象或类的公共结构,通常推荐使用接口;对于其他类型的命名,推荐使用类型别名。

3.6 联合类型(Union Types)

联合类型允许一个变量可以是几种类型之一。使用 | 符号连接不同的类型。

“`typescript
let result: number | string; // result 可以是数字或字符串

result = 100; // OK
result = “success”; // OK
// result = true; // 编译错误:不能将类型“boolean”分配给类型“string | number”。
“`

在使用联合类型变量时,需要小心,只能访问它们共有属性或方法,或者通过类型守卫(Type Guards)缩小类型范围。

3.7 交叉类型(Intersection Types)

交叉类型允许将多个类型合并为一个类型,它包含了所有类型的特性。使用 & 符号连接不同的类型。

“`typescript
interface Serializable {
serialize(): string;
}

interface Deserializable {
deserialize(data: string): this; // 返回当前类型的实例
}

// 创建一个既可序列化又可反序列化的类型
type Persistent = Serializable & Deserializable;

// 实现 Persistent 类型需要同时包含 serialize 和 deserialize 方法
class DataObject implements Persistent {
data: any;
constructor(data: any) { this.data = data; }
serialize(): string { return JSON.stringify(this.data); }
deserialize(data: string): this {
this.data = JSON.parse(data);
return this;
}
}
“`
交叉类型在组合现有类型时非常有用。

3.8 字面量类型(Literal Types)

字面量类型允许你指定一个变量只能是某个特定的字面量值(字符串、数字、布尔值)之一。

“`typescript
let direction: “up” | “down” | “left” | “right”; // direction 只能是这四个字符串之一

direction = “up”; // OK
// direction = “forward”; // 编译错误

let status: 0 | 1 | 2; // status 只能是 0, 1 或 2

function processStatus(s: status) {
// …
}
“`
字面量类型常与联合类型结合使用,用于限制变量的可能值范围。

3.9 类型推断(Type Inference)

TypeScript 编译器非常智能,它在很多情况下可以根据上下文自动“推断”出变量的类型,无需你显式地添加类型注解。

“`typescript
let count = 10; // TypeScript 推断 count 的类型是 number
// count = “twenty”; // 编译错误:不能将类型“string”分配给类型“number”。

let message = “hello world”; // TypeScript 推断 message 的类型是 string

const PI = 3.14159; // const 声明的变量,其值不会改变,TypeScript 推断其类型是字面量类型 3.14159

let arr = [1, 2, 3]; // TypeScript 推断 arr 的类型是 number[]
// arr.push(“four”); // 编译错误
“`

利用类型推断可以减少代码中的类型注解,让代码更简洁,同时依然享受到类型检查的好处。只有在类型推断不准确或你需要明确指定类型(比如函数参数、函数返回值、复杂对象结构等)时,才需要手动添加类型注解。

4. 使用 TypeScript 的好处总结

回顾一下,使用 TypeScript 带来的主要优势包括:

  1. 更早发现错误: 在编译阶段而非运行时捕获类型错误,大大减少了生产环境中的 Bug。
  2. 提高代码质量和可维护性: 清晰的类型定义使得代码意图更明确,易于理解、修改和重构。
  3. 增强工具支持: 编辑器能够提供强大的智能提示、代码补全、导航和重构功能,显著提升开发效率。
  4. 改善团队协作: 明确的接口和类型定义是团队成员之间沟通代码结构、约定数据格式的有力工具。
  5. 适应大型项目和复杂应用: 类型系统为构建和维护大型、复杂的代码库提供了必要的结构和约束。

5. 如何开始使用 TypeScript(入门实践)

踏出 TypeScript 的第一步非常简单。

5.1 安装 TypeScript

你需要 Node.js 环境来安装 TypeScript 编译器。如果你还没有安装 Node.js,请先前往 Node.js 官网 下载安装。

安装 TypeScript 全局命令行工具:

bash
npm install -g typescript

安装完成后,你可以在终端中运行 tsc -v 来检查安装是否成功以及 TypeScript 的版本。

5.2 编写你的第一个 TypeScript 文件

创建一个新文件,命名为 hello.ts,并输入以下代码:

“`typescript
// hello.ts
function greet(person: string) {
return “Hello, ” + person;
}

let user = “TypeScript User”;

console.log(greet(user));

// 尝试传入错误类型,看看会发生什么(暂不运行)
// console.log(greet(123));
“`

5.3 编译 TypeScript 代码

打开终端,切换到存放 hello.ts 文件的目录,然后运行 TypeScript 编译器命令:

bash
tsc hello.ts

如果代码没有类型错误,tsc 命令会静默执行成功,并在同目录下生成一个 hello.js 文件。打开这个 .js 文件,你会看到 TypeScript 类型注解已经被移除,只剩下纯 JavaScript 代码:

javascript
// hello.js
function greet(person) {
return "Hello, " + person;
}
var user = "TypeScript User";
console.log(greet(user));
// 尝试传入错误类型,看看会发生什么(暂不运行)
// console.log(greet(123));

现在你可以像运行普通 JavaScript 文件一样运行这个 hello.js 文件:

bash
node hello.js

输出应该是:Hello, TypeScript User

如果你取消 console.log(greet(123)); 的注释,然后再次运行 tsc hello.ts,编译器会报错:

“`
hello.ts:9:23 – error TS2345: Argument of type ‘number’ is not assignable to parameter of type ‘string’.

9 console.log(greet(123));
~~~

Found 1 error in the mentioned files.
“`

这就是 TypeScript 在编译阶段发现类型错误的能力!在运行代码之前,你就已经知道了问题所在。

5.4 配置项目 (tsconfig.json)

对于更复杂的项目,你不会只编译一个文件。TypeScript 允许你通过一个 tsconfig.json 文件来配置编译选项,例如目标 JavaScript 版本、模块系统、严格类型检查等级等。

在一个项目根目录,你可以运行以下命令生成一个默认的 tsconfig.json 文件:

bash
tsc --init

这会在当前目录创建一个 tsconfig.json 文件,其中包含许多注释掉的选项。你可以根据项目需要修改这些配置。一个推荐的配置是开启严格模式,这能提供最全面的类型检查:

“`json
{
“compilerOptions”: {
/ Language and Environment Options /
“target”: “es2016”, / Specify ECMAScript target version: ‘ES3’ (default), ‘ES5’, ‘ES2015’, ‘ES2016’, ‘ES2017’, ‘ES2018’, ‘ES2019’, ‘ES2020’, ‘ES2021’, ‘ES2022’, ‘ESNext’. /
“module”: “commonjs”, / Specify module code generation: ‘none’, ‘commonjs’, ‘amd’, ‘system’, ‘umd’, ‘es2015’, ‘es2020’, ‘es2022’, ‘市镇’, ‘nodenext’. /

/* Modules */
// "rootDir": "./", /* Specify the root folder within your source files. */

/* Emit */
// "outDir": "./dist", /* Specify an output folder for all emitted files. */

/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// ... 其他选项

},
// “include”: [“src//.ts”], / Specify files to include in compilation /
// “exclude”: [“node_modules”] /
Specify files to exclude from compilation */
}
“`

有了 tsconfig.json 文件后,你只需在项目根目录运行 tsc 命令(不带文件名),编译器就会查找 tsconfig.json 并按照其中的配置编译整个项目中的 .ts 文件。

5.5 集成到构建流程和框架

在实际开发中,你通常会将 TypeScript 集成到你的前端构建流程中(如 Webpack, Rollup)或直接使用支持 TypeScript 的框架(如 Angular, Vue CLI, Create React App)。这些工具通常已经配置好了 TypeScript 的编译和打包流程,让你更便捷地在项目中使用 TypeScript。许多现代框架创建项目时就提供了 TypeScript 模板选项。

6. 从 JavaScript 平滑过渡

如果你有一个现有的 JavaScript 项目,不用担心,你可以逐步引入 TypeScript:

  1. .js 文件逐个重命名为 .ts.tsx (如果使用 React)。
  2. .ts 文件中开始添加类型注解。你可以从最容易的部分开始,比如函数的参数和返回值,或者关键的数据结构。
  3. 利用 any 作为临时的过渡,当你还不确定或不想立即为某个部分添加严格类型时。但记得后面再来完善。
  4. 配置 tsconfig.json,可以先从非严格模式开始,逐步开启更严格的检查选项。
  5. TypeScript 文件和 JavaScript 文件可以共存在同一个项目中,你可以逐步转换。

结论

TypeScript 为 JavaScript 带来了强大的静态类型检查能力,它不是一门全新的语言,而是 JavaScript 的增强版。通过在代码中加入类型信息,TypeScript 能够帮助我们在开发早期发现潜在错误,提高代码的可读性、可维护性和开发效率。

虽然学习 TypeScript 需要投入一些时间去理解其类型系统,但投入是值得的。对于任何中大型项目或团队协作开发而言,TypeScript 都能带来显著的优势。

现在,你已经了解了 TypeScript 是什么以及它的核心概念和入门方法。最重要的是动手去尝试!创建一个小项目,或者把你现有项目的一部分代码转换为 TypeScript,亲身体验它带来的便利和价值。祝你在 TypeScript 的旅程中一切顺利!

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部