TypeScript核心要点:JavaScript开发者的实战指南
面向 JavaScript 开发者的 TypeScript 实战指南:核心类型、tsconfig 配置、严格模式,以及逐步迁移方案,让代码日后更易于修改。
来源: TypeScript for JavaScript Developers: A Practical Beginner’s Guide
如果你已经熟悉 JavaScript,学习 TypeScript 就不像是在学习一门全新的语言,而更像是扩展你已掌握的这门语言。
TypeScript 本质上是基于 JavaScript 加入了类型系统后的增强版本。
你依然可以继续使用变量、函数、数组、对象、类、Promise、async/await、模块以及所有其他你已熟悉的元素。TypeScript 的优势在于提供了一套工具,能够在代码在浏览器或服务器上运行之前就提前发现错误。
本指南将介绍对已有 JavaScript 开发经验的人最相关的 TypeScript 概念。
什么是 TypeScript?
TypeScript 是一种构建在 JavaScript 之上的语言。
在大多数情况下,你编写的任何 JavaScript 程序也都是有效的 TypeScript 代码。TypeScript 的优势在于它提供了静态类型检查功能,最终会将代码编译为可在浏览器和 Node.js 中运行的普通 JavaScript 代码。
以这段简单的 JavaScript 代码为例:
function add(a, b) {
return a + b;
}
用 TypeScript 重写后,同一个函数可以声明它接受何种类型的值:
function add(a: number, b: number): number {
return a + b;
}
具体说明如下:
a: number要求a必须是数字类型。b: number要求b也必须是数字类型。- 末尾的
: number表示该函数的返回值是数字类型。
有了这些注解,TypeScript 可以在代码执行之前就检测出错误的函数调用:
add(10, 20); // Correct
add("10", "20"); // TypeScript error
在纯 JavaScript 中,使用字符串参数调用该函数会静默地返回 "1020",因为加号运算符会连接字符串而非对数字进行求和。TypeScript 能够提前发现这类意外行为。
为何要使用 TypeScript?
JavaScript 的灵活性是其优势,但同时也可能引发一些隐蔽的错误。
以这个函数为例:
function calculateDiscount(price, percentage) {
return price - price * percentage / 100;
}
正确调用时,其效果如预期般正常:
calculateDiscount(1000, 10);
但你也完全可以这样调用它:
calculateDiscount("1000", "ten");
问题只有在代码实际执行时才会显现出来。
使用 TypeScript 后,你可以指定函数期望的参数类型:
function calculateDiscount(
price: number,
percentage: number
): number {
return price - price * percentage / 100;
}
这样 TypeScript 就能在无需先运行代码的情况下立即标记出无效参数。
它带来的主要优势包括:
- 在编写代码时就能发现错误
- 编辑器中的自动补全功能得到提升
- 重构更简单、更安全
- 函数和对象的结构更加清晰
- 在较大的代码库中进行协作更为顺畅
- 类型本身即可作为内置文档
当项目文件数量超过少数几个或涉及多名贡献者时,TypeScript的优势就会显得尤为明显。
安装 TypeScript
可以通过以下方式全局安装 TypeScript:
npm install -g typescript
要确认已安装的版本,可运行:
tsc --version
然后创建一个名为:
app.ts
在其中添加一些 TypeScript 代码:
const message: string = "Hello TypeScript";
console.log(message);
使用以下命令编译该文件:
tsc app.ts
这样就会生成编译后的 JavaScript 输出:
app.js
你可以直接使用 Node.js 运行生成的结果:
node app.js
在生产环境中,React、Next.js、Angular 和 NestJS 等工具和框架通常会自动为你处理这个编译步骤。
tsconfig.json 文件
大多数 TypeScript 项目都依赖 tsconfig.json 文件来配置编译器。
你可以通过以下方式生成该文件:
tsc --init
最简的配置可能如下所示:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
以下是其中一些较为重要的设置:
target
该选项用于指定编译器输出的 JavaScript 版本。
"target": "ES2020"
module
该选项用于设置编译输出所使用的模块格式。
"module": "ESNext"
strict
开启此选项可启用更严格的类型检查。
"strict": true
对于任何新项目,强烈建议保持严格模式开启状态。
rootDir
该参数用于告知编译器 TypeScript 源文件的位置。
"rootDir": "./src"
outDir
它决定了编译后的 JavaScript 输出文件应保存的位置。
"outDir": "./dist"
基本类型
TypeScript 提供了多种你已熟悉的原始类型。
字符串
let username: string = "Vijeet";
数字
let experience: number = 10;
TypeScript 不区分整数和浮点数;二者都属于 number 类型。
let age: number = 35;
let price: number = 999.99;
布尔值
let isActive: boolean = true;
空值与未定义值
let emptyValue: null = null;
let missingValue: undefined = undefined;
在严格模式下,你需要更谨慎地处理 null 和 undefined。
类型推断
不必手动为每个值添加注释。
TypeScript 通常可以根据赋给变量的值来判断其类型。
let city = "Ahmedabad";
在这里,TypeScript 会自动将 city 识别为字符串。
尝试像这样重新赋值会引发错误:
city = 100;
这里的逻辑相同:
let total = 500;
TypeScript 推断 total 存储的是数字。
依靠类型推断可以让代码更简洁。只有在确实需要提高可读性的地方,比如函数参数、API响应结构以及较为复杂的对象结构中,才使用显式的类型注释。
数组
这是一个普通的 JavaScript 数组:
const skills = ["JavaScript", "React", "Node.js"];
TypeScript 允许你为数组的元素类型添加注解:
const skills: string[] = [
"JavaScript",
"React",
"Node.js"
];
你也可以使用泛型语法来写它:
const skills: Array<string> = [
"JavaScript",
"React",
"Node.js"
];
同样的原理也适用于数字数组:
const scores: number[] = [75, 82, 91];
TypeScript 会阻止任何尝试插入错误类型值的操作:
scores.push(95); // Correct
scores.push("ninety"); // Error
对象
你可以为普通的 JavaScript 对象指定明确的结构。
const user: {
name: string;
age: number;
isAdmin: boolean;
} = {
name: "Vijeet",
age: 35,
isAdmin: true
};
如果某个属性缺失或类型不正确,TypeScript 会将其标记为错误。
const user: {
name: string;
age: number;
} = {
name: "Vijeet"
};
这里,对象中缺少必需的 age 属性。
当需要在多个地方重复使用相同的对象结构时,接口或类型别名比每次都手动编写结构更为合适。
接口
接口定义了对象应具有的结构。
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
一旦定义,即可在任何地方重用该接口:
const user: User = {
id: 1,
name: "Vijeet",
email: "vijeet@example.com",
isActive: true
};
接口在以下场景中非常有用:
- 描述用户的存储信息
- 对目录项进行建模
- 验证表单中输入的字段
- 规范从 API 调用返回的数据结构
- 指定传递给组件的属性类型
- 表示数据库表中的一行数据
例如:
interface Product {
id: number;
title: string;
price: number;
}
此时函数可以将 Product 作为参数接收:
function displayProduct(product: Product): void {
console.log(`${product.title}: ₹${product.price}`);
}
可选属性
某些对象属性并非始终存在。
只需在属性名称后加上问号即可标记它们:
interface User {
id: number;
name: string;
phone?: string;
}
以下两个对象都符合该接口的要求:
const userOne: User = {
id: 1,
name: "Amit"
};
const userTwo: User = {
id: 2,
name: "Neha",
phone: "9876543210"
};
由于 phone 是可选的,其类型变为:
string | undefined
这意味着在使用它之前应先验证其是否存在:
if (userTwo.phone) {
console.log(userTwo.phone);
}
类型别名
类型别名允许你为任何类型指定一个可重复使用的名称。
type UserId = number;
之后就可以将该别名当作类型来使用:
const id: UserId = 101;
在描述联合类型或更复杂的类型组合时,类型别名能发挥巨大作用。
type Status = "pending" | "approved" | "rejected";
使用此别名后,只允许三种特定的字符串:
let applicationStatus: Status = "pending";
尝试使用其他任何内容都会引发错误:
applicationStatus = "completed";
接口与类型别名
接口和类型别名都能用来描述对象的结构。
这是接口版本:
interface Customer {
name: string;
email: string;
}
以及对应的类型别名版本:
type Customer = {
name: string;
email: string;
};
给新手的一个大致准则:
- 在描述对象或类结构时,使用
interface。 - 在处理联合类型、原始类型的别名、元组或更复杂的组合类型时,使用
type。
不过这并非绝对规则,在很多情况下两种方法的效果都一样好。
联合类型
联合类型允许一个值取多种可能的类型中的一种。
let userId: string | number;
以下这两种赋值都是合法的:
userId = 101;
userId = "USR-101";
当来自现实世界的数据并不总是遵循固定格式时,联合类型非常有用。
function printId(id: string | number): void {
console.log(id);
}
在调用仅存在于某种可能类型上的方法之前,需要先缩小值的范围:
function printId(id: string | number): void {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(0));
}
}
这种技术被称为类型收窄。
字面量类型
字面量类型将变量限制在一组精确的、预定义的值中。
type Theme = "light" | "dark" | "system";
这种赋值是可行的:
let selectedTheme: Theme = "dark";
但这种则会被拒绝:
selectedTheme = "blue";
字面量类型非常适合以下场景:
- 记录的当前状态
- 分配给用户的权限等级
- 视觉主题设置
- 按钮的视觉风格
- 请求中使用的HTTP动词
- 应用程序的运行模式
例如:
type UserRole = "admin" | "editor" | "customer";
函数
对于函数参数,类型紧跟在参数名称之后。
function greet(name: string): string {
return `Hello, ${name}`;
}
函数的返回类型位于参数列表的闭合括号之后。
function multiply(a: number, b: number): number {
return a * b;
}
当函数不返回值时,用void进行标注。
function logMessage(message: string): void {
console.log(message);
}
可选参数用 ? 标记:
function greetUser(
name: string,
title?: string
): string {
if (title) {
return `Hello, ${title} ${name}`;
}
return `Hello, ${name}`;
}
默认参数值的行为与普通 JavaScript 中完全相同:
function calculateTax(
amount: number,
rate: number = 18
): number {
return amount * rate / 100;
}
箭头函数的类型标注
可以以相同方式对箭头函数进行标注。
const subtract = (
a: number,
b: number
): number => {
return a - b;
};
这是一种更简洁的形式:
const subtract = (
a: number,
b: number
): number => a - b;
any 类型
将 any 类型赋值给某个变量,意味着告诉 TypeScript 完全停止对该变量的检查。
let data: any = "Hello";
data = 100;
data = true;
data = { name: "Vijeet" };
虽然这乍看之下很方便,但过度使用 any 会浪费 TypeScript 的大部分功能。
请看这个例子:
function processData(data: any): any {
return data.someProperty.someMethod();
}
对于这个函数中的错误,TypeScript 无能为力。
仅在确实无法避免的情况下才使用 any —— 例如在逐步转换 JavaScript 代码库时,或是处理缺乏适当类型定义的第三方数据时。
unknown 类型
unknown 为表示那些类型在事先无法确定的值提供了一种更安全的方式。
let value: unknown;
与 any 一样,它也能接受各种类型的值:
value = "Hello";
value = 100;
value = true;
但与 any 不同,TypeScript 要求你在实际使用之前先限定该类型的范围:
if (typeof value === "string") {
console.log(value.toUpperCase());
}
正因如此,unknown 非常适合用于:
- 来自外部 API 的数据
- 从 JSON 解析得到的值
- 在 try/catch 块中捕获的错误对象
- 用户输入的数据
- 任何结构尚未确定的值
类型断言
有时,关于某个值的信息比 TypeScript 自行能推断的更多。
类型断言允许你告诉编译器如何处理该值。
const input = document.getElementById(
"email"
) as HTMLInputElement;
一旦进行断言,就可以访问该类型特有的属性:
console.log(input.value);
请记住,断言不会在运行时改变任何内容——它仅会改变 TypeScript 在编译时对该值的解读方式。
因此,应谨慎使用断言,且仅在确定数据的实际结构时才使用。
元组
元组是一种具有固定数量位置的数组,每个位置都有其定义好的类型。
let userEntry: [number, string];
userEntry = [1, "Vijeet"];
此处,第一个位置必须存放数字,第二个位置必须存放字符串。
以下写法将会出错:
userEntry = ["Vijeet", 1];
元组非常适合处理小型、结构固定的数据或成对的值:
type Coordinate = [number, number];
const location: Coordinate = [23.0225, 72.5714];
当数据变得更为复杂或需要具有描述性的字段名时,普通对象通常更清晰。
枚举
枚举允许你定义一组固定的命名常量。
enum OrderStatus {
Pending,
Processing,
Shipped,
Delivered
}
你可以这样引用它们:
let status: OrderStatus = OrderStatus.Processing;
为枚举赋予字符串值通常能提高可读性:
enum PaymentStatus {
Pending = "pending",
Paid = "paid",
Failed = "failed"
}
尽管如此,许多 TypeScript 用户仍更倾向于使用联合类型,尤其是在较简单的场景中:
type PaymentStatus =
| "pending"
| "paid"
| "failed";
联合类型通常会生成更少的 JavaScript 代码,是大多数前端代码的理想默认选择。
类
TypeScript 在普通 JavaScript 类的基础上增加了类型检查功能。
class Employee {
name: string;
salary: number;
constructor(name: string, salary: number) {
this.name = name;
this.salary = salary;
} getAnnualSalary(): number {
return this.salary * 12;
}
}
类的实例化方式与 JavaScript 中相同:
const employee = new Employee("Rahul", 50000);
console.log(employee.getAnnualSalary());
访问修饰符也是 TypeScript 类语法的一部分。
class BankAccount {
public accountHolder: string;
private balance: number;
constructor(accountHolder: string, balance: number) {
this.accountHolder = accountHolder;
this.balance = balance;
} public getBalance(): number {
return this.balance;
}
}
主要的修饰符有:
public:可从任何地方访问private:仅可在类内部访问protected:可在类及其所有子类中访问readonly:可设置一次,之后不可重新赋值
泛型
泛型能让您编写灵活且可重用的代码,同时在不同输入类型之间保持完全的类型安全。
以这个函数作为起点:
function returnValue(value: any): any {
return value;
}
由于它使用了 any,TypeScript 无法告知其返回值的任何信息。
使用通用参数重写代码可以解决该问题:
function returnValue<T>(value: T): T {
return value;
}
现在返回类型会与传入的类型保持一致:
const text = returnValue<string>("Hello");
const count = returnValue<number>(100);
在许多情况下,TypeScript可以自动推断出通用类型,因此无需手动指定:
const text = returnValue("Hello");
const count = returnValue(100);
在处理数组、API响应结构、可复用UI组件、自定义钩子以及辅助工具时,经常会用到通用类型。
function getFirstItem<T>(items: T[]): T | undefined {
return items[0];
}
使用方式如下:
const firstNumber = getFirstItem([10, 20, 30]);
const firstName = getFirstItem(["Amit", "Neha"]);
Promise与异步函数
使用async声明的函数始终会返回Promise。
async function getUsername(): Promise<string> {
return "Vijeet";
}
一个返回用户对象的异步函数可能长这样:
interface User {
id: number;
name: string;
}
async function getUser(): Promise<User> {
return {
id: 1,
name: "Vijeet"
};
}
而对于用户列表,则需要将返回类型封装在数组中:
async function getUsers(): Promise<User[]> {
return [
{ id: 1, name: "Vijeet" },
{ id: 2, name: "Rahul" }
];
}
以这种方式为 API 返回值指定正确的类型,可以避免其他代码部分出现后续错误。
为 API 响应指定类型
想象一个返回产品记录的接口:
interface Product {
id: number;
title: string;
price: number;
}
你可以将 fetch 调用封装在函数中,并在该函数中声明其返回类型:
async function fetchProducts(): Promise<Product[]> {
const response = await fetch("/api/products");
if (!response.ok) {
throw new Error("Unable to fetch products");
} const data: Product[] = await response.json(); return data;
}
有一点需要记住:一旦应用程序开始运行,TypeScript 就无法验证这些数据了。
这行代码:
const data: Product[] = await response.json();
仅指示编译器将解析后的响应视为 Product[] —— 它并不能确保服务器确实返回了那种结构的对象。
对于那些正确性至关重要的应用程序,开发人员通常会将 TypeScript 与 Zod 等运行时验证库结合使用来弥补这一缺陷。
处理 DOM 元素
浏览器元素类型是 TypeScript 内置的。
const button = document.querySelector(
"#submit-button"
);
编译器知道该查询可能会返回空结果,因此结果类型会同时包含元素类型和 null。
这意味着在使用之前应先处理缺失值的情况:
if (button) {
button.addEventListener("click", () => {
console.log("Button clicked");
});
}
如果需要更具体的元素类型,可以对其进行断言:
const emailInput = document.querySelector(
"#email"
) as HTMLInputElement | null;
在确认其存在后即可读取其值:
if (emailInput) {
console.log(emailInput.value);
}
TypeScript 无法替代运行时验证
这一点尤其需要强调,尤其是对于直接从 JavaScript 转型而来的开发者。
TypeScript 的检查仅发生在开发阶段和编译阶段。一旦代码被编译为 JavaScript,所有的类型注解都会消失。
以这个函数为例:
function double(value: number): number {
return value * 2;
}
编译后,它大致会变成这样:
function double(value) {
return value * 2;
}
此时,已不存在可用于强制执行任何规则的原始类型信息了。
因此,TypeScript无法保证:
- API确实返回了格式正确的数据
- 用户输入的是有效值
- 数据库记录符合预期的结构
- 第三方服务遵守了其文档中规定的契约
任何来自代码外部的内容都仍需在程序运行时进行验证。
迁移现有的JavaScript项目
将JavaScript代码库转换为TypeScript并不一定非得一次性彻底重写。
实际操作中,逐步迁移的方式往往更有效。
步骤1:安装TypeScript
npm install --save-dev typescript
步骤2:创建tsconfig.json
npx tsc --init
步骤3:暂时允许使用JavaScript
{
"compilerOptions": {
"allowJs": true,
"checkJs": false
}
}
第4步:逐步重命名文件
将某个文件像这样修改:
utils.js
变为:
utils.ts
对于React组件,则将:
Button.jsx
改为:
Button.tsx
第5步:一次修复一个文件中的错误
建议从工具函数、共享数据模型以及较小的组件开始处理,将更为复杂的应用逻辑留到后面解决。
这种逐步推进的方式正是TypeScript在集成到现有项目时所带来的最大实际优势之一。
JavaScript开发者常犯的错误
到处使用any
const user: any = getUser();
这虽然能暂时避免编译器的警告,但也会同时取消所有的类型检查。
更好的做法是定义一个实际的接口:
interface User {
id: number;
name: string;
}
为每个变量添加类型
没有必要写下这些内容:
const name: string = "Vijeet";
const count: number = 10;
const active: boolean = true;
因为类型推断已经能够自动确定这些类型:
const name = "Vijeet";
const count = 10;
const active = true;
只有在确实能提升清晰度的场合才使用显式注解。
忽略 null
这段代码在运行时可能会出现问题:
const element = document.querySelector("#app");
element.innerHTML = "Hello";
被查询的元素可能根本就不存在于页面上。
应当提前防范这种可能性:
const element = document.querySelector("#app");
if (element) {
element.innerHTML = "Hello";
}
利用类型断言掩盖问题
编写这样的代码存在风险:
const user = apiResponse as User;
类型断言并不检查实际 API 响应的任何内容——它只是让编译器相信你的说法。
将 TypeScript 视为不同的运行时环境
TypeScript不会被浏览器直接执行。在运行之前,它会被编译成普通的JavaScript。
这意味着所有关于JavaScript的现有知识依然适用。
实际示例
想象一个简单的购物车功能,用通常的JavaScript方式实现,但现在加入了TypeScript类型定义。
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
function calculateCartTotal(
items: CartItem[]
): number {
return items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
}
const cart: CartItem[] = [
{
id: 1,
name: "Keyboard",
price: 2500,
quantity: 1
},
{
id: 2,
name: "Mouse",
price: 1200,
quantity: 2
}
];
const total = calculateCartTotal(cart);
console.log(`Cart total: ₹${total}`);
有了这些类型定义后,该函数就能避免因购物车数据格式错误而出现问题。
例如,以下代码片段将无法编译:
const invalidCart: CartItem[] = [
{
id: 1,
name: "Keyboard",
price: "2500",
quantity: 1
}
];
问题在于price被设置为字符串,而它应该是数字。
每个JavaScript项目都需要使用TypeScript吗?
并非总是如此。
对于以下情况,普通的JavaScript依然可以正常使用:
- 小型实验
- 简单脚本
- 快速原型开发
- 基础着陆页
在以下情况下,TypeScript 的优势更为明显:
- 大型前端应用
- React 和 Next.js 代码库
- Node.js 后端服务
- 跨团队共享的代码库
- 长期运行的产品
- 处理复杂数据结构的项目
- 由多名开发者共同构建和维护的应用程序
一般来说,项目规模越大、生命周期越长,采用 TypeScript 能带来的好处就越多。
总结
TypeScript 并不能替代 JavaScript —— 它通过为代码添加额外的安全层来提升编写 JavaScript 的体验。
作为从 JavaScript 转型而来的人,你已经具备了大部分必要的基础知识:
- 变量
- 函数
- 对象
- 数组
- 类
接下来需要学习的主要是如何表达数据的结构。
良好的起点包括:
- 原始类型
- 函数的参数和返回类型
- 接口
- 联合类型
- 可选属性
- 泛型
- API的类型化响应
不必试图立刻掌握所有高级特性。先从为日常的JavaScript代码添加注解开始,让实际项目的需求来指引你后续的学习方向。
一旦TypeScript变得驾轻就熟,再回到纯的、无类型的JavaScript时,可能会感到出乎意料的不稳定。