TypeScript 基础教程:为 JavaScript 添加类型安全
TypeScript 是由微软开发的开源编程语言,它是 JavaScript 的超集,在 JavaScript 的基础上添加了静态类型系统。TypeScript 代码在编译时会进行类型检查,并最终转换为纯 JavaScript 代码,可以在任何支持 JavaScript 的环境中运行。本文将从零开始带你学习 TypeScript 的核心概念,掌握类型系统的精髓。
一、TypeScript 简介与安装
1.1 为什么需要 TypeScript
JavaScript 是一种动态类型语言,变量的类型在运行时才能确定。这带来了一些问题:
- 类型错误只能在运行时发现
- 重构时容易出错,IDE 难以提供有效提示
- 大型项目维护困难,代码可读性差
TypeScript 通过静态类型系统解决了这些问题:
- 编译时类型检查:在编译阶段就能发现大部分类型错误
- 更好的 IDE 支持:智能补全、重构、跳转定义等功能更强
- 代码即文档:类型注解本身就是最好的文档
- 渐进式增强:可以逐步为现有 JS 项目添加类型
1.2 安装 TypeScript
# 全局安装 TypeScript 编译器
npm install -g typescript
# 验证安装
tsc --version
# 也可以在项目中本地安装
npm install --save-dev typescript
# 初始化 TypeScript 配置文件
tsc --init
1.3 第一个 TypeScript 程序
// hello.ts
function greet(name: string): string {
return `Hello, ${name}!`;
}
const message: string = greet('平平');
console.log(message);
# 编译为 JavaScript
tsc hello.ts
# 编译后会生成 hello.js,然后运行
node hello.js
# 输出: Hello, 平平!
1.4 tsconfig.json 配置
{
"compilerOptions": {
"target": "ES2020", // 编译目标 JS 版本
"module": "commonjs", // 模块系统
"strict": true, // 启用所有严格类型检查
"esModuleInterop": true, // 允许 ES 模块互操作
"skipLibCheck": true, // 跳过类型库检查
"forceConsistentCasingInFileNames": true,
"outDir": "./dist", // 输出目录
"rootDir": "./src", // 源代码目录
"declaration": true, // 生成 .d.ts 声明文件
"sourceMap": true // 生成 source map
},
"include": ["src/**/*"], // 包含的文件
"exclude": ["node_modules", "dist"] // 排除的文件
}
二、基本类型
2.1 原始类型
// 布尔值
let isDone: boolean = false;
// 数字(包括整数和浮点数)
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
// 字符串
let color: string = 'blue';
let fullName: string = `平平`;
let sentence: string = `Hello, my name is ${fullName}.`;
// null 和 undefined
let u: undefined = undefined;
let n: null = null;
// symbol(ES6)
let sym: symbol = Symbol('key');
2.2 数组
// 第一种写法:类型 + []
let numbers: number[] = [1, 2, 3, 4, 5];
let strings: string[] = ['a', 'b', 'c'];
// 第二种写法:泛型数组
let list: Array<number> = [1, 2, 3];
let names: Array<string> = ['Alice', 'Bob'];
// 只读数组
let readOnlyArray: ReadonlyArray<number> = [1, 2, 3];
// readOnlyArray[0] = 4; // 报错:只读数组不能修改
2.3 元组(Tuple)
元组表示已知元素数量和类型的数组,各元素类型可以不同:
// 定义元组:[string, number]
let person: [string, number] = ['平平', 25];
console.log(person[0]); // '平平'
console.log(person[1]); // 25
// person[0] = 123; // 报错:必须是 string
// person = ['平平']; // 报错:缺少 number 元素
// 可选元素的元组
let optionalTuple: [string, number?] = ['Alice'];
optionalTuple = ['Bob', 30];
2.4 枚举(Enum)
// 数字枚举(默认从 0 开始)
enum Color {
Red, // 0
Green, // 1
Blue // 2
}
let c: Color = Color.Green;
console.log(c); // 1
// 自定义起始值
enum Direction {
Up = 1,
Down, // 2
Left, // 3
Right // 4
}
// 字符串枚举
enum Status {
Pending = 'PENDING',
Success = 'SUCCESS',
Failed = 'FAILED'
}
let status: Status = Status.Success;
console.log(status); // 'SUCCESS'
// 反向映射(仅数字枚举)
enum Weekday {
Monday = 1,
Tuesday,
Wednesday
}
console.log(Weekday[1]); // 'Monday'
console.log(Weekday.Monday); // 1
// const 枚举(编译时内联,性能更好)
const enum LogLevel {
Debug,
Info,
Warning,
Error
}
const level: LogLevel = LogLevel.Info; // 编译后直接是 1
2.5 any、unknown、void、never
// any:任意类型(放弃类型检查,不推荐使用)
let notSure: any = 4;
notSure = 'maybe a string';
notSure = false;
// unknown:安全的 any(必须先检查类型才能使用)
let uncertain: unknown = 4;
// uncertain.toFixed(); // 报错:unknown 类型不能直接使用
if (typeof uncertain === 'number') {
console.log(uncertain.toFixed(2)); // 类型检查后可以使用
}
// void:无返回值(常用于函数)
function logMessage(msg: string): void {
console.log(msg);
}
// never:永不存在的值的类型
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
三、接口与类型别名
3.1 接口(Interface)
接口用于定义对象的形状,是 TypeScript 中最常用的类型定义方式:
// 基本接口
interface User {
name: string;
age: number;
email?: string; // 可选属性
readonly id: number; // 只读属性
}
const user: User = {
id: 1,
name: '平平',
age: 25,
// email 可以不提供
};
// user.id = 2; // 报错:只读属性不能修改
// 接口继承
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
const dog: Dog = {
name: '旺财',
breed: '金毛',
bark() {
console.log('Woof!');
}
};
// 函数类型接口
interface SearchFunc {
(source: string, subString: string): boolean;
}
const mySearch: SearchFunc = function(src, sub) {
return src.includes(sub);
};
// 可索引类型
interface StringArray {
[index: number]: string;
}
const arr: StringArray = ['a', 'b', 'c'];
3.2 类型别名(Type Alias)
// 基本类型别名
type Name = string;
type Age = number;
// 对象类型别名
type User = {
name: string;
age: number;
email?: string;
readonly id: number;
};
// 联合类型别名
type ID = string | number;
type Status = 'pending' | 'success' | 'failed';
// 函数类型别名
type Callback = (data: any) => void;
// 使用类型别名
const userId: ID = 12345;
const userName: Name = '平平';
const handleResponse: Callback = (data) => console.log(data);
3.3 interface 与 type 的区别
- 扩展方式:interface 使用
extends,type 使用&交叉类型 - 声明合并:interface 可以重复声明并合并,type 不行
- 适用范围:type 可以定义原始类型、联合类型等,interface 主要用于对象
// interface 声明合并
interface Window {
customProp: string;
}
interface Window {
anotherProp: number;
}
// 最终 Window 类型包含 customProp 和 anotherProp
// type 交叉类型扩展
type Animal = {
name: string;
};
type Dog = Animal & {
breed: string;
};
四、函数类型
4.1 函数声明
// 为参数和返回值添加类型
function add(x: number, y: number): number {
return x + y;
}
// 可选参数(用 ? 标记,必须在必选参数之后)
function greet(name: string, greeting?: string): string {
return `${greeting || 'Hello'}, ${name}!`;
}
greet('平平'); // 'Hello, 平平!'
greet('平平', 'Hi'); // 'Hi, 平平!'
// 默认参数
function greetWithDefault(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`;
}
// 剩余参数
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
4.2 函数重载
// 重载签名
function reverse(x: number): number;
function reverse(x: string): string;
// 实现签名
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else {
return x.split('').reverse().join('');
}
}
const num: number = reverse(12345); // 54321
const str: string = reverse('hello'); // 'olleh'
五、泛型
泛型(Generics)允许我们编写可复用的、类型安全的代码。它像是类型的"参数",让我们可以在使用时再指定具体类型。
5.1 泛型函数
// 泛型函数:T 是类型参数
function identity<T>(arg: T): T {
return arg;
}
// 显式指定类型
const result1: string = identity<string>('hello');
const result2: number = identity<number>(42);
// 类型推断(推荐,让 TS 自动推断)
const result3 = identity('world'); // 推断为 string
const result4 = identity(true); // 推断为 boolean
// 多个类型参数
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair('hello', 42); // [string, number]
// 泛型约束:限制类型必须有某些属性
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength('hello'); // 5
logLength([1, 2, 3]); // 3
// logLength(42); // 报错:number 没有 length 属性
5.2 泛型接口与类
// 泛型接口
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: 'hello' };
const numberBox: Box<number> = { value: 42 };
// 泛型类
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
const stringStack = new Stack<string>();
stringStack.push('a');
stringStack.push('b');
5.3 泛型工具类型
// Partial:所有属性变为可选
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = Partial<User>;
// 等价于 { id?: number; name?: string; age?: number; }
// Required:所有属性变为必选
type RequiredUser = Required<PartialUser>;
// Readonly:所有属性变为只读
type ReadonlyUser = Readonly<User>;
// Pick:挑选部分属性
type UserBasic = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }
// Omit:排除部分属性
type UserWithoutId = Omit<User, 'id'>;
// { name: string; age: number; }
// Record:键值对类型
type UserMap = Record<string, User>;
// ReturnType:获取函数返回值类型
function getUser() {
return { name: '平平', age: 25 };
}
type UserType = ReturnType<typeof getUser>;
六、联合类型与交叉类型
6.1 联合类型
联合类型表示一个值可以是多种类型之一:
// 基本联合类型
let id: string | number;
id = 123;
id = 'abc';
// 字面量联合类型
type Direction = 'left' | 'right' | 'up' | 'down';
function move(direction: Direction): void {
console.log(`向${direction}移动`);
}
move('left'); // OK
// move('diagonal'); // 报错
// 联合类型与 null/undefined
function greet(name: string | null) {
if (name === null) {
console.log('Hello, stranger!');
} else {
console.log(`Hello, ${name}!`);
}
}
6.2 交叉类型
交叉类型将多个类型合并为一个类型:
interface BusinessPartner {
name: string;
credit: number;
}
interface Identity {
id: number;
name: string;
}
interface Contact {
email: string;
phone: string;
}
// 交叉类型:同时满足多个接口
type Employee = Identity & Contact;
type Customer = BusinessPartner & Contact;
const employee: Employee = {
id: 1,
name: '平平',
email: 'ping@example.com',
phone: '1234567890'
};
七、类型守卫
类型守卫(Type Guards)用于在条件块中缩小类型的范围:
7.1 typeof 类型守卫
function padLeft(value: string, padding: string | number): string {
// typeof 缩小类型范围
if (typeof padding === 'number') {
return ' '.repeat(padding) + value; // padding 被识别为 number
}
return padding + value; // padding 被识别为 string
}
7.2 instanceof 类型守卫
class Cat {
meow(): void { console.log('Meow!'); }
}
class Dog {
bark(): void { console.log('Woof!'); }
}
function speak(pet: Cat | Dog): void {
// instanceof 缩小类型范围
if (pet instanceof Cat) {
pet.meow(); // pet 被识别为 Cat
} else {
pet.bark(); // pet 被识别为 Dog
}
}
7.3 in 类型守卫
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function move(pet: Fish | Bird) {
// in 操作符检查属性是否存在
if ('swim' in pet) {
pet.swim(); // pet 被识别为 Fish
} else {
pet.fly(); // pet 被识别为 Bird
}
}
7.4 自定义类型守卫
interface ApiSuccess {
success: true;
data: string;
}
interface ApiError {
success: false;
error: string;
}
type ApiResponse = ApiSuccess | ApiError;
// 自定义类型谓词函数
function isSuccess(response: ApiResponse): response is ApiSuccess {
return response.success === true;
}
function handleResponse(response: ApiResponse) {
if (isSuccess(response)) {
console.log(response.data); // response 被识别为 ApiSuccess
} else {
console.log(response.error); // response 被识别为 ApiError
}
}
八、实战案例:用 TS 重构一个 API 接口
下面我们用 TypeScript 实现一个类型安全的 API 请求封装:
// types/api.ts — 定义类型
// API 响应基础结构
interface BaseResponse<T> {
code: number;
message: string;
timestamp: number;
}
// 成功响应
interface SuccessResponse<T> extends BaseResponse<T> {
code: 200;
success: true;
data: T;
}
// 错误响应
interface ErrorResponse extends BaseResponse<never> {
code: 400 | 401 | 403 | 404 | 500;
success: false;
error: string;
}
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
// 用户相关类型
interface User {
id: number;
username: string;
email: string;
avatar: string;
createdAt: string;
}
interface UserListParams {
page?: number;
pageSize?: number;
keyword?: string;
}
interface PaginatedData<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
// 文章相关类型
interface Article {
id: number;
title: string;
content: string;
author: User;
tags: string[];
publishedAt: string;
}
type ArticleCreateData = Omit<Article, 'id' | 'author' | 'publishedAt'>;
type ArticleUpdateData = Partial<ArticleCreateData>;
// HTTP 方法类型
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
// 请求配置
interface RequestOptions {
method?: HttpMethod;
headers?: Record<string, string>;
body?: unknown;
params?: Record<string, string | number | boolean | undefined>;
}
// utils/http.ts — HTTP 请求封装
class HttpClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
this.defaultHeaders = {
'Content-Type': 'application/json'
};
}
// 设置认证 token
setAuthToken(token: string | null): void {
if (token) {
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
} else {
delete this.defaultHeaders['Authorization'];
}
}
// 构建完整 URL
private buildUrl(path: string, params?: RequestOptions['params']): string {
const url = new URL(`${this.baseUrl}${path}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, String(value));
}
});
}
return url.toString();
}
// 通用请求方法
private async request<T>(
path: string,
options: RequestOptions = {}
): Promise<ApiResponse<T>> {
const { method = 'GET', headers = {}, body, params } = options;
try {
const response = await fetch(this.buildUrl(path, params), {
method,
headers: { ...this.defaultHeaders, ...headers },
body: body ? JSON.stringify(body) : undefined
});
const data: ApiResponse<T> = await response.json();
return data;
} catch (error) {
return {
code: 500,
success: false,
message: '网络请求失败',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: Date.now()
};
}
}
// GET 请求
get<T>(path: string, params?: RequestOptions['params']): Promise<ApiResponse<T>> {
return this.request<T>(path, { method: 'GET', params });
}
// POST 请求
post<T>(path: string, body?: unknown): Promise<ApiResponse<T>> {
return this.request<T>(path, { method: 'POST', body });
}
// PUT 请求
put<T>(path: string, body?: unknown): Promise<ApiResponse<T>> {
return this.request<T>(path, { method: 'PUT', body });
}
// DELETE 请求
delete<T>(path: string): Promise<ApiResponse<T>> {
return this.request<T>(path, { method: 'DELETE' });
}
}
// services/userService.ts — 用户服务
const http = new HttpClient('https://api.example.com');
// 类型守卫:检查是否为成功响应
function isSuccessResponse<T>(res: ApiResponse<T>): res is SuccessResponse<T> {
return res.success === true;
}
export const userService = {
// 获取用户列表
async getUsers(params: UserListParams = {}): Promise<PaginatedData<User>> {
const response = await http.get<PaginatedData<User>>('/users', {
page: params.page ?? 1,
pageSize: params.pageSize ?? 10,
keyword: params.keyword
});
if (isSuccessResponse(response)) {
return response.data;
}
throw new Error(response.error);
},
// 获取单个用户
async getUserById(id: number): Promise<User> {
const response = await http.get<User>(`/users/${id}`);
if (isSuccessResponse(response)) {
return response.data;
}
throw new Error(response.error);
},
// 创建用户
async createUser(data: Omit<User, 'id' | 'createdAt'>): Promise<User> {
const response = await http.post<User>('/users', data);
if (isSuccessResponse(response)) {
return response.data;
}
throw new Error(response.error);
},
// 更新用户
async updateUser(id: number, data: Partial<User>): Promise<User> {
const response = await http.put<User>(`/users/${id}`, data);
if (isSuccessResponse(response)) {
return response.data;
}
throw new Error(response.error);
},
// 删除用户
async deleteUser(id: number): Promise<void> {
const response = await http.delete<void>(`/users/${id}`);
if (!isSuccessResponse(response)) {
throw new Error(response.error);
}
}
};
export const articleService = {
// 获取文章列表
async getArticles(page: number = 1): Promise<PaginatedData<Article>> {
const response = await http.get<PaginatedData<Article>>('/articles', { page });
if (isSuccessResponse(response)) {
return response.data;
}
throw new Error(response.error);
},
// 创建文章
async createArticle(data: ArticleCreateData): Promise<Article> {
const response = await http.post<Article>('/articles', data);
if (isSuccessResponse(response)) {
return response.data;
}
throw new Error(response.error);
},
// 更新文章
async updateArticle(id: number, data: ArticleUpdateData): Promise<Article> {
const response = await http.put<Article>(`/articles/${id}`, data);
if (isSuccessResponse(response)) {
return response.data;
}
throw new Error(response.error);
}
};
// 使用示例
async function main() {
try {
// 获取用户列表
const result = await userService.getUsers({
page: 1,
pageSize: 20,
keyword: '平平'
});
console.log(`共 ${result.total} 个用户`);
result.list.forEach(user => {
console.log(`${user.id}: ${user.username} (${user.email})`);
});
// 创建文章
const newArticle = await articleService.createArticle({
title: 'TypeScript 入门',
content: '这是一篇关于 TypeScript 的文章...',
tags: ['TypeScript', '教程']
});
console.log(`文章创建成功,ID: ${newArticle.id}`);
// 更新文章
const updated = await articleService.updateArticle(newArticle.id, {
title: 'TypeScript 进阶'
});
console.log(`文章已更新: ${updated.title}`);
} catch (error) {
console.error('操作失败:', error instanceof Error ? error.message : error);
}
}
main();
九、总结
通过本文的学习,你应该掌握了 TypeScript 的核心概念:
- TypeScript 的安装与配置(tsconfig.json)
- 基本类型:原始类型、数组、元组、枚举、any/unknown/void/never
- 接口(Interface)与类型别名(Type Alias)的使用与区别
- 函数类型:参数类型、返回值类型、函数重载
- 泛型:泛型函数、泛型接口、泛型类、工具类型
- 联合类型与交叉类型
- 类型守卫:typeof、instanceof、in、自定义类型谓词
- 实战:用 TS 构建类型安全的 API 请求封装
TypeScript 的核心价值在于在编译阶段就发现潜在错误,让代码更加健壮、可维护。建议在实际项目中逐步应用,从简单的类型注解开始,逐渐深入到泛型、高级类型等概念。配合 IDE 的智能提示,TypeScript 能大幅提升开发效率和代码质量。