TypeScript编程从入门到实战手把手教你写出高质量代码零基础也能快速上手
第一章 初识TypeScript:别怕,它比你想象的要温柔
如果你从来没有碰过TypeScript,第一次看到下面这种代码可能会有点懵:
const userName: string = "张三";
const age: number = 25;
const isStudent: boolean = false;
const hobbies: string[] = ["编程", "读书", "跑步"];
“这些冒号是什么?string[]又是什么鬼?”——别急,我也是这么过来的。TypeScript其实就是在JavaScript的基础上加了一层”类型检查”的语法糖,让你写代码的时候就像有个细心的朋友在旁边提醒你:”嘿,这个变量应该是字符串,不是数字哦。”
为什么需要TypeScript?
先来看一个真实场景。假设你正在写一个计算总价的函数:
// JavaScript 版本
function calculateTotal(price, quantity) {
return price * quantity;
}
console.log(calculateTotal(10, 5)); // 50 ✓
console.log(calculateTotal("10", 5)); // "105" (字符串拼串了!)
console.log(calculateTotal(10, "五")); // NaN
看到了吗?在JavaScript里,"10"和10是完全不同的东西,但JS不会阻止你混用它们。等程序跑起来,可能你已经找了半天bug,才发现是因为传错了参数类型。
TypeScript能在你写代码的时候就揪出这些问题:
// TypeScript 版本
function calculateTotal(price: number, quantity: number): number {
return price * quantity;
}
console.log(calculateTotal(10, 5)); // 50 ✓
console.log(calculateTotal("10", 5)); // ❌ 编译报错:类型"string"不能赋给类型"number"
console.log(calculateTotal(10, "五")); // ❌ 编译报错:类型"string"不能赋给类型"number"
是不是清爽多了?你还没跑代码,TypeScript就已经帮你把问题拦下来了。
TypeScript和JavaScript的关系
很多人以为TypeScript是一门新语言,其实它只是JavaScript的超集。这意味着:所有合法的JavaScript代码,都是合法的TypeScript代码。你可以在TypeScript文件里直接写JS代码,只是没有类型检查而已。
这就好比你在写文章时,先有草稿(JavaScript),再用红笔批改(TypeScript)。最终发布出去的还是那篇文章,只是经过修改后更精准了。
第二章 环境搭建:五分钟搞定
你不需要安装什么复杂的IDE或者花大价钱买工具。一个免费的VS Code编辑器,加上TypeScript编译器,就够了。
安装TypeScript编译器
打开你的终端(Windows用PowerShell或CMD,Mac用Terminal),输入:
npm install -g typescript
安装完成后,可以用下面的命令检查是否成功:
tsc --version
看到类似Version 5.3.3的输出,就说明安装好了。
创建第一个TypeScript项目
创建一个文件夹,比如叫ts-learning,然后里面建一个index.ts文件:
// index.ts
const greeting: string = "你好,TypeScript!";
console.log(greeting);
在终端运行:
tsc index.ts
你会发现同级目录下多了一个index.js文件,这就是TypeScript编译后的JavaScript。运行它:
node index.js
输出:你好,TypeScript!
使用tsconfig.json自动编译
每次都手动输入tsc index.ts太麻烦了。我们可以创建一个配置文件,让TypeScript自动帮我们编译:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
把这个文件放在项目根目录。之后只需要:
tsc
TypeScript就会自动读取配置,编译src目录下的所有.ts文件,输出到dist目录。
小建议:
strict: true这个选项一定要打开。它会启用最严格的类型检查,虽然一开始会报很多错,但能帮你养成好习惯。
第三章 基础类型:把变量”管”起来
TypeScript的类型系统分很多种,我们一个一个来,从最简单的开始。
原始类型
TypeScript提供了和JavaScript一样的原始类型,只是多了类型注解:
// 字符串
let name: string = "李华";
let age: string = "25"; // 注意:虽然内容是数字,但类型是字符串
// 数字(整数、浮点数、二进制、十六进制都行)
let score: number = 98;
let price: number = 3.14;
let hex: number = 0xff; // 255
let binary: number = 0b1010; // 10
// 布尔值
let isLoggedIn: boolean = true;
let isVIP: boolean = false;
// null 和 undefined
let nothing: null = null;
let notSet: undefined = undefined;
// symbol(很少用到,知道有这东西就行)
let id: symbol = Symbol("id");
注意:
null和undefined在TypeScript里是特殊的类型。默认情况下,它们可以赋给任何类型。如果你开了strictNullChecks,就需要额外处理它们(后面会讲到)。
任意类型:什么时候该用
有时候你就是不知道变量的类型,或者你要处理第三方库的数据,这时候可以用any:
let data: any = "hello";
data = 100;
data = true;
data = { name: "张三" };
any就像一个”万能容器”,什么都能装。但它也有副作用:TypeScript会放弃对它的类型检查。
let something: any = "hello";
something = 100;
something.toUpperCase(); // TypeScript不会报错,但运行时会崩(因为数字没有toUpperCase方法)
所以any要慎用,只在万不得已的时候用。如果实在不知道类型,可以考虑用unknown(后面会讲)。
数组类型
有两种写法:
// 写法一:类型后加方括号
let scores: number[] = [90, 85, 78, 92, 88];
let names: string[] = ["张三", "李四", "王五"];
// 写法二:使用Array泛型
let scores2: Array<number> = [90, 85, 78, 92, 88];
let names2: Array<string> = ["张三", "李四", "王五"];
两种写法完全等价,第一种更常用,更简洁。
元组类型:固定长度的数组
元组(Tuple)是一种特殊的数组,它的长度固定,每个位置上的类型也固定:
// 元组:第一个元素是字符串,第二个是数字
let person: [string, number] = ["张三", 25];
// 这样写会报错,因为第二个元素应该是数字
let person2: [string, number] = ["李四", "二十"]; // ❌
// 这样写也会报错,因为元组长度固定为2
let person3: [string, number] = ["王五", 30, "程序员"]; // ❌
元组在日常开发中经常用于返回多个值:
function getUserInfo(): [string, number] {
return ["赵六", 28];
}
let [userName, userAge] = getUserInfo();
console.log(userName); // "赵六"
console.log(userAge); // 28
枚举类型:让魔法数字消失
你有没有见过这样的代码?
const STATUS_ACTIVE = 0;
const STATUS_INACTIVE = 1;
const STATUS_PENDING = 2;
function getStatusName(status) {
if (status === 0) return "激活";
if (status === 1) return "未激活";
if (status === 2) return "待处理";
}
用数字表示状态,可读性很差。TypeScript的枚举(Enum)就是为了解决这个问题:
enum Status {
Active = 0,
Inactive = 1,
Pending = 2
}
function getStatusName(status: Status): string {
switch (status) {
case Status.Active:
return "激活";
case Status.Inactive:
return "未激活";
case Status.Pending:
return "待处理";
}
}
console.log(getStatusName(Status.Active)); // "激活"
console.log(getStatusName(0)); // 编译报错:类型"number"不能赋给类型"Status"
枚举有两种:数字枚举和字符串枚举:
// 数字枚举(默认)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
// 字符串枚举
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
// 混合枚举
enum AppStatus {
Loading = "LOADING",
Success = "SUCCESS",
Error = "ERROR",
Retry = 0 // 也可以混用数字和字符串
}
小贴士:字符串枚举更安全,因为它不容易出现”0既表示加载又表示错误”的混淆。建议在可能的情况下优先使用字符串枚举。
第四章 对象类型:接口(Interface)与类型别名(Type Alias)
这是TypeScript最强大的功能之一。当你需要描述一个对象的形状时,有两种选择:接口和类型别名。
接口:描述对象的”骨架”
接口用interface关键字定义,专门用来描述对象的结构:
interface User {
name: string;
age: number;
email: string;
isActive: boolean;
}
const user: User = {
name: "张三",
age: 25,
email: "zhangsan@example.com",
isActive: true
};
// 缺少字段会报错
const incompleteUser: User = {
name: "李四"
// ❌ 报错:缺少 age、email、isActive
};
// 多了字段也会报错(严格模式下)
const extraUser: User = {
name: "王五",
age: 30,
email: "wangwu@example.com",
isActive: true,
extra: "这是多余的字段"
// ❌ 报错:对象字面量只能指定已知属性
};
可选属性
有些字段不是必须的,可以用?标记为可选:
interface Article {
title: string;
content: string;
author?: string; // 可选字段
publishDate?: Date; // 可选字段
tags?: string[]; // 可选字段
}
const article1: Article = {
title: "TypeScript入门",
content: "这是一篇入门文章"
// author、publishDate、tags 都是可选的,可以不传
};
const article2: Article = {
title: "TypeScript进阶",
content: "这是一篇进阶文章",
author: "李四",
publishDate: new Date("2024-01-01"),
tags: ["编程", "TypeScript"]
};
只读属性
有些字段一旦设置就不应该被修改,可以用readonly标记:
interface Config {
readonly apiUrl: string;
readonly timeout: number;
debug: boolean; // 这个字段可以修改
}
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: false
};
config.apiUrl = "https://new-api.example.com"; // ❌ 报错:Cannot assign to 'apiUrl' because it is a read-only property
config.debug = true; // ✓ 可以修改
类型别名:更灵活的类型描述
类型别名用type关键字定义,它可以描述任何东西,不只是对象:
// 别名一个对象类型
type User = {
name: string;
age: number;
email: string;
};
// 别名一个联合类型
type ID = string | number;
// 别名一个函数类型
type Callback = (data: string) => void;
// 别名一个字符串字面量类型
type Direction = "up" | "down" | "left" | "right";
接口 vs 类型别名:怎么选?
这是一个经常争论的问题。简单来说:
- 用接口:当你需要描述对象的形状,或者需要
extends(继承)和implements(实现)时。 - 用类型别名:当你需要描述联合类型、元组类型、或者函数类型时。
实际上,对于简单的对象描述,两者可以互换。但如果需要扩展,接口更方便:
interface BaseUser {
name: string;
age: number;
}
// 接口可以继承
interface AdminUser extends BaseUser {
role: string;
}
// 类型别名不能直接继承,需要交叉类型
type BaseUser2 = {
name: string;
age: number;
};
type AdminUser2 = BaseUser2 & {
role: string;
};
第五章 函数类型:让参数和返回值清清楚楚
TypeScript对函数的类型支持非常完善,从参数到返回值,从可选参数到剩余参数,全覆盖。
基本函数类型
// 普通函数
function add(a: number, b: number): number {
return a + b;
}
// 箭头函数
const multiply = (a: number, b: number): number => {
return a * b;
};
// 函数类型别名(可以用来描述函数)
type MathFunction = (a: number, b: number) => number;
const divide: MathFunction = (a, b) => {
return a / b;
};
可选参数和默认参数
// 可选参数(用?标记,且必须放在必需参数后面)
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting},${name}!`;
}
return `你好,${name}!`;
}
console.log(greet("张三")); // "你好,张三!"
console.log(greet("李四", "早上好")); // "早上好,李四!"
// 默认参数
function greetWithDefault(name: string, greeting: string = "你好"): string {
return `${greeting},${name}!`;
}
console.log(greetWithDefault("王五")); // "你好,王五!"
console.log(greetWithDefault("赵六", "早安")); // "早安,赵六!"
注意:可选参数和默认参数不能同时用在同一个参数上,TypeScript会报错。
剩余参数
当参数数量不确定时,用剩余参数(Rest Parameters):
function sum(...numbers: number[]): number {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
函数重载:同一个函数,不同行为
函数重载是TypeScript的一个高级特性,它允许同一个函数名有多个签名,根据传入参数的不同,返回不同类型的值:
// 函数重载签名
function parseInput(input: string): number;
function parseInput(input: string[]): string[];
function parseInput(input: any): any {
if (typeof input === "string") {
return parseInt(input);
} else if (Array.isArray(input)) {
return input.map(item => item.toUpperCase());
}
}
console.log(parseInput("123")); // 123
console.log(parseInput(["a", "b"])); // ["A", "B"]
函数重载在实际开发中很有用,比如处理不同输入格式的API:
// 模拟一个获取用户信息的API
interface User {
id: number;
name: string;
email: string;
}
// 重载签名
function getUser(id: number): Promise<User>;
function getUser(name: string): Promise<User>;
function getUser(query: number | string): Promise<User> {
// 模拟异步请求
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1,
name: typeof query === "number" ? "张三" : query,
email: "zhangsan@example.com"
});
}, 100);
});
}
// TypeScript能根据传入参数的类型推断返回值
getUser(1).then(user => console.log(user.name)); // 类型安全
getUser("李四").then(user => console.log(user.name)); // 类型安全
第六章 泛型:写一次代码,服务多种类型
泛型是TypeScript最强大也最容易被忽视的功能。它的核心思想是:让类型也变成参数,这样你就能写出既灵活又类型安全的代码。
为什么需要泛型?
先来看一个没有泛型的问题:
// 问题:我们想写一个函数,返回传入的参数
// 但不用泛型的话,只能指定一个具体类型
function identity(arg: string): string {
return arg;
}
// 如果我想传数字呢?只能再写一个函数
function identityNumber(arg: number): number {
return arg;
}
这就很麻烦,如果我要支持所有类型呢?用any?
function identityAny(arg: any): any {
return arg;
}
// 这样写确实能运行,但失去了类型安全
let result = identityAny(42);
result.toUpperCase(); // TypeScript不报错,但运行时会崩
泛型就是为了解决这个问题:
// 用泛型:T代表任意类型
function identity<T>(arg: T): T {
return arg;
}
// 使用泛型函数
let result1 = identity<number>(42); // 显式指定类型为number
let result2 = identity<string>("hello"); // 显式指定类型为string
let result3 = identity(42); // TypeScript会自动推断类型为number
// 类型安全!
result1.toFixed(2); // ✓ 数字有toFixed方法
result2.toUpperCase(); // ✓ 字符串有toUpperCase方法
result1.toUpperCase(); // ❌ 报错:number上不存在toUpperCase方法
泛型接口和泛型类
泛型不只可以用于函数,还可以用于接口和类:
// 泛型接口
interface Response<T> {
data: T;
status: number;
message: string;
}
// 使用泛型接口
interface UserResponse {
data: {
id: number;
name: string;
};
status: number;
message: string;
}
// 用泛型简化
type ApiResponse<T> = Response<T>;
const userResponse: ApiResponse<{ id: number; name: string }> = {
data: { id: 1, name: "张三" },
status: 200,
message: "成功"
};
泛型类也是类似的:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
get items(): T[] {
return this.items;
}
}
// 使用
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // 返回number类型
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.pop(); // 返回string类型
泛型约束:限制泛型的范围
有时候我们希望泛型只能接受某些特定的类型。比如,我们想写一个函数,只接受有length属性的类型(字符串、数组等):
// 定义一个约束接口
interface HasLength {
length: number;
}
// 用extends约束泛型
function getLength<T extends HasLength>(arg: T): T & HasLength {
console.log(`长度为: ${arg.length}`);
return arg;
}
getLength("hello"); // ✓ 字符串有length属性
getLength([1, 2, 3]); // ✓ 数组有length属性
getLength(42); // ❌ 报错:number没有length属性
更实用的例子:处理API响应,限制泛型必须是对象:
interface BaseEntity {
id: number;
}
// 只允许有id属性的对象
function findById<T extends BaseEntity>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
const users = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" }
];
const user = findById(users, 1);
console.log(user?.name); // "张三"
第七章 高级类型:联合类型、交叉类型、类型断言
TypeScript提供了一些高级类型操作符,能让你的类型表达更加精确。
联合类型:多种类型的”或”
联合类型用|表示,意味着变量可以是其中任意一种类型:
// 变量可以是字符串或数字
let value: string | number;
value = "hello"; // ✓
value = 42; // ✓
value = true; // ❌ 报错:类型"boolean"不能赋给类型"string | number"
联合类型经常和类型守卫一起使用:
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
// 在这里,TypeScript知道padding是number类型
return " ".repeat(padding) + value;
}
// 在这里,TypeScript知道padding是string类型
return padding + value;
}
交叉类型:多种类型的”与”
交叉类型用&表示,意味着类型同时具有多个类型的特征:
interface Animal {
name: string;
eat(): void;
}
interface Bird {
name: string;
fly(): void;
}
// 交叉类型:同时具有Animal和Bird的特征
type SuperBird = Animal & Bird;
const superBird: SuperBird = {
name: "超鸟",
eat() {
console.log("吃东西");
},
fly() {
console.log("飞翔");
}
};
superBird.eat(); // ✓
superBird.fly(); // ✓
交叉类型在实际开发中常用于合并多个对象:
function merge<T, U>(first: T, second: U): T & U {
return { ...first, ...second };
}
const config = {
timeout: 5000,
retry: 3
};
const defaults = {
timeout: 3000,
debug: false
};
const merged = merge(config, defaults);
// merged的类型是 { timeout: number; retry: number } & { timeout: number; debug: boolean }
// 简化后是 { timeout: number; retry: number; debug: boolean }
类型断言:告诉TypeScript”相信我”
有时候你知道变量的类型,但TypeScript不知道。这时可以用类型断言:
// 从DOM获取元素,TypeScript不知道具体类型
const element = document.getElementById("myElement");
// 类型断言:告诉TypeScript这是HTMLDivElement
const div = element as HTMLDivElement;
div.style.color = "red"; // ✓
// 或者用尖括号语法(在JSX中不能用)
const div2 = <HTMLDivElement>element;
div2.style.color = "blue"; // ✓
注意:类型断言只是告诉TypeScript”我相信这个类型是对的”,但不会做运行时检查。用错了也不会报错,只是运行时会出问题。所以不要滥用类型断言。
类型收窄: narrowing
类型收窄是指通过条件判断,缩小变量的类型范围:
function processValue(value: string | number | null) {
// 第一层收窄:排除null
if (value !== null) {
// 现在value是 string | number
console.log(value);
}
// 第二层收窄:区分string和number
if (typeof value === "string") {
// 现在value是 string
console.log(value.toUpperCase());
} else if (typeof value === "number") {
// 现在value是 number
console.log(value.toFixed(2));
}
}
常见的类型收窄方式:
typeof检查instanceof检查in操作符检查- 相等性检查(
===、!==) - 自定义类型守卫函数
// 自定义类型守卫
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // ✓ TypeScript知道pet是Fish
} else {
pet.fly(); // ✓ TypeScript知道pet是Bird
}
}
第八章 工具类型:站在巨人的肩膀上
TypeScript内置了很多工具类型,能帮你快速构建复杂类型。最常用的有:Partial、Required、Readonly、Pick、Omit、Record、Exclude、Extract等。
Partial:所有属性变为可选
interface User {
name: string;
age: number;
email: string;
}
// Partial<User> = { name?: string; age?: number; email?: string; }
function updateUser(partialUser: Partial<User>): void {
console.log(partialUser);
}
updateUser({ name: "张三" }); // ✓
updateUser({ age: 25, email: "z@x.com" }); // ✓
updateUser({}); // ✓ 全部可选
这个工具类型在更新部分字段时非常有用。
Required:所有属性变为必需
interface Config {
apiUrl?: string;
timeout?: number;
debug?: boolean;
}
// Required<Config> = { apiUrl: string; timeout: number; debug: boolean; }
function initConfig(config: Required<Config>): void {
console.log(config);
}
initConfig({
apiUrl: "https://api.example.com",
timeout: 5000,
debug: true
});
Readonly:所有属性变为只读
interface Point {
x: number;
y: number;
}
// Readonly<Point> = { readonly x: number; readonly y: number; }
function movePoint(point: Readonly<Point>): void {
point.x = 10; // ❌ 报错:Cannot assign to 'x' because it is a read-only property
}
Pick:挑选部分属性
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Pick<User, 'id' | 'name' | 'email'> = { id: number; name: string; email: string; }
type UserDisplay = Pick<User, 'id' | 'name' | 'email'>;
const userDisplay: UserDisplay = {
id: 1,
name: "张三",
email: "zhangsan@example.com"
};
这个工具类型非常适合用于API响应,只暴露需要的字段:
// 定义用户类型
interface User {
id: number;
name: string;
email: string;
password: string;
role: string;
createdAt: Date;
updatedAt: Date;
}
// API返回的应该是Pick,不包括敏感信息
interface UserApiResponse {
data: Pick<User, 'id' | 'name' | 'email' | 'role' | 'createdAt'>;
}
Omit:排除部分属性
// Omit<User, 'password'> = { id: number; name: string; email: string; role: string; createdAt: Date; updatedAt: Date; }
type SafeUser = Omit<User, 'password'>;
Omit和Pick是互补的。Pick是”挑选想要”,Omit是”排除不想要”。
Record:创建映射类型
// Record<string, number> = { [key: string]: number }
type ScoreMap = Record<string, number>;
const scores: ScoreMap = {
math: 90,
chinese: 85,
english: 92
};
// Record也可以用联合类型作为键
type Status = "loading" | "success" | "error";
type ApiResponse<T> = Record<Status, T>;
const response: ApiResponse<string> = {
loading: "加载中...",
success: "成功",
error: "失败"
};
Record经常用来创建字典或映射表。
Exclude和Extract:从联合类型中筛选
type Status = "pending" | "loading" | "success" | "error";
// Exclude<Status, "pending" | "error"> = "loading" | "success"
type SuccessStatus = Exclude<Status, "pending" | "error">;
// Extract<Status, "pending" | "error"> = "pending" | "error"
type AsyncStatus = Extract<Status, "pending" | "error">;
这两个工具类型在处理状态机时特别有用。
第九章 实战:从0到1搭建一个完整的TypeScript项目
光说不练假把式。下面我们来搭建一个小型项目,把前面学到的知识都用上。
项目概览
我们将创建一个简单的”任务管理工具”,包含以下功能:
- 添加任务
- 标记完成
- 筛选任务
- 持久化存储(用localStorage)
项目结构
task-manager/
├── src/
│ ├── types/
│ │ └── index.ts # 类型定义
│ ├── tasks/
│ │ ├── types.ts # 任务相关类型
│ │ ├── service.ts # 任务业务逻辑
│ │ └── storage.ts # 存储逻辑
│ ├── ui/
│ │ └── index.ts # UI渲染
│ └── index.ts # 入口文件
├── tsconfig.json
├── package.json
└── README.md
第一步:定义类型
// src/types/index.ts
// 任务状态
export enum TaskStatus {
Pending = "pending",
InProgress = "in_progress",
Completed = "completed"
}
// 任务接口
export interface Task {
id: string;
title: string;
description: string;
status: TaskStatus;
createdAt: Date;
updatedAt: Date;
tags: string[];
priority: "low" | "medium" | "high";
}
// 任务创建参数(使用Partial,因为id和日期是自动生成的)
export type TaskCreateInput = Omit<Task, 'id' | 'createdAt' | 'updatedAt'>;
// 任务更新参数(只允许更新部分字段)
export type TaskUpdateInput = Partial<Omit<Task, 'id' | 'createdAt' | 'updatedAt'>>;
// API响应类型
export interface ApiResponse<T> {
success: boolean;
data: T;
message: string;
}
// 分页参数
export interface PaginationParams {
page: number;
pageSize: number;
}
// 分页响应
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
第二步:实现存储层
// src/tasks/storage.ts
import { Task, TaskCreateInput, TaskUpdateInput } from "../types";
const STORAGE_KEY = "task_manager_tasks";
// 工具函数:生成唯一ID
function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
// 读取所有任务
function getTasks(): Task[] {
const data = localStorage.getItem(STORAGE_KEY);
if (!data) return [];
try {
return JSON.parse(data) as Task[];
} catch {
return [];
}
}
// 保存所有任务
function saveTasks(tasks: Task[]): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
// 添加任务
export function addTask(input: TaskCreateInput): Task {
const tasks = getTasks();
const now = new Date();
const newTask: Task = {
...input,
id: generateId(),
createdAt: now,
updatedAt: now
};
tasks.push(newTask);
saveTasks(tasks);
return newTask;
}
// 更新任务
export function updateTask(id: string, input: TaskUpdateInput): Task | null {
const tasks = getTasks();
const index = tasks.findIndex(task => task.id === id);
if (index === -1) return null;
const updated = {
...tasks[index],
...input,
id: tasks[index].id, // 防止id被修改
createdAt: tasks[index].createdAt, // 防止createdAt被修改
updatedAt: new Date()
};
tasks[index] = updated;
saveTasks(tasks);
return updated;
}
// 删除任务
export function deleteTask(id: string): boolean {
const tasks = getTasks();
const filtered = tasks.filter(task => task.id !== id);
if (filtered.length === tasks.length) return false;
saveTasks(filtered);
return true;
}
// 获取单个任务
export function getTask(id: string): Task | undefined {
return getTasks().find(task => task.id === id);
}
第三步:实现业务逻辑
// src/tasks/service.ts
import { Task, TaskUpdateInput, TaskStatus, PaginationParams, PaginatedResponse } from "../types";
import * as storage from "./storage";
// 任务服务类,封装所有业务逻辑
class TaskService {
// 添加任务
addTask(title: string, description: string, priority: Task["priority"], tags: string[]): Task {
return storage.addTask({
title,
description,
status: TaskStatus.Pending,
priority,
tags
});
}
// 更新任务
updateTask(id: string, input: TaskUpdateInput): Task | null {
return storage.updateTask(id, input);
}
// 标记任务为完成
completeTask(id: string): Task | null {
return this.updateTask(id, { status: TaskStatus.Completed });
}
// 删除任务
deleteTask(id: string): boolean {
return storage.deleteTask(id);
}
// 获取所有任务
getAllTasks(): Task[] {
return storage.getTasks();
}
// 筛选任务
filterTasks(filters: {
status?: TaskStatus;
priority?: Task["priority"];
tag?: string;
keyword?: string;
}): Task[] {
let tasks = this.getAllTasks();
if (filters.status) {
tasks = tasks.filter(task => task.status === filters.status);
}
if (filters.priority) {
tasks = tasks.filter(task => task.priority === filters.priority);
}
if (filters.tag) {
tasks = tasks.filter(task => task.tags.includes(filters.tag!));
}
if (filters.keyword) {
tasks = tasks.filter(task =>
task.title.includes(filters.keyword!) ||
task.description.includes(filters.keyword!)
);
}
return tasks;
}
// 分页查询
paginateTasks(params: PaginationParams, filters?: Parameters<TaskService["filterTasks"]>[0]): PaginatedResponse<Task> {
const tasks = this.filterTasks(filters ?? {});
const { page, pageSize } = params;
const totalPages = Math.ceil(tasks.length / pageSize);
const start = (page - 1) * pageSize;
const items = tasks.slice(start, start + pageSize);
return {
items,
total: tasks.length,
page,
pageSize,
totalPages
};
}
// 获取统计信息
getStatistics(): {
total: number;
pending: number;
inProgress: number;
completed: number;
byPriority: Record<Task["priority"], number>;
} {
const tasks = this.getAllTasks();
const byPriority: Record<Task["priority"], number> = {
low: 0,
medium: 0,
high: 0
};
tasks.forEach(task => {
byPriority[task.priority]++;
});
return {
total: tasks.length,
pending: tasks.filter(t => t.status === TaskStatus.Pending).length,
inProgress: tasks.filter(t => t.status === TaskStatus.InProgress).length,
completed: tasks.filter(t => t.status === TaskStatus.Completed).length,
byPriority
};
}
}
// 导出单例
export const taskService = new TaskService();
第四步:实现UI层
// src/ui/index.ts
import { taskService } from "../tasks/service";
import { Task, TaskStatus } from "../types";
class TaskUI {
private container: HTMLElement;
constructor(containerId: string) {
this.container = document.getElementById(containerId)!;
this.render();
}
// 渲染整个界面
render(): void {
const tasks = taskService.getAllTasks();
const stats = taskService.getStatistics();
this.container.innerHTML = `
<div class="task-manager">
<h1>任务管理器</h1>
<div class="stats">
<span>总计: ${stats.total}</span>
<span>待办: ${stats.pending}</span>
<span>进行中: ${stats.inProgress}</span>
<span>已完成: ${stats.completed}</span>
</div>
<div class="task-list">
${this.renderTaskList(tasks)}
</div>
<button onclick="window.addTaskUI()">+ 添加任务</button>
</div>
`;
}
// 渲染任务列表
private renderTaskList(tasks: Task[]): string {
if (tasks.length === 0) {
return '<p class="empty">暂无任务</p>';
}
return tasks.map(task => `
<div class="task-card ${task.status}">
<h3>${this.escapeHtml(task.title)}</h3>
<p>${this.escapeHtml(task.description)}</p>
<div class="task-meta">
<span class="priority priority-${task.priority}">${this.getPriorityLabel(task.priority)}</span>
<span class="status">${this.getStatusLabel(task.status)}</span>
<span class="date">${task.createdAt.toLocaleDateString()}</span>
</div>
<div class="tags">
${task.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join("")}
</div>
<div class="actions">
${task.status !== TaskStatus.Completed ? `<button onclick="window.markComplete('${task.id}')">完成</button>` : ""}
<button onclick="window.deleteTask('${task.id}')">删除</button>
</div>
</div>
`).join("");
}
// 工具方法:转义HTML
private escapeHtml(text: string): string {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// 工具方法:获取优先级标签
private getPriorityLabel(priority: Task["priority"]): string {
const labels: Record<Task["priority"], string> = {
low: "低",
medium: "中",
high: "高"
};
return labels[priority];
}
// 工具方法:获取状态标签
private getStatusLabel(status: TaskStatus): string {
const labels: Record<TaskStatus, string> = {
[TaskStatus.Pending]: "待办",
[TaskStatus.InProgress]: "进行中",
[TaskStatus.Completed]: "已完成"
};
return labels[status];
}
}
// 导出单例
export const taskUI = new TaskUI("app");
第五步:入口文件
// src/index.ts
import { taskService } from "./tasks/service";
import { taskUI } from "./ui";
// 全局函数,供HTML内联事件调用
window.addTask = () => {
const title = prompt("请输入任务标题:");
if (!title) return;
const description = prompt("请输入任务描述:") || "";
const priority = prompt("请输入优先级(low/medium/high):") as Task["priority"] || "medium";
const tags = prompt("请输入标签(逗号分隔):")?.split(",").map(t => t.trim()) || [];
taskService.addTask(title, description, priority, tags);
taskUI.render();
};
window.markComplete = (id: string) => {
taskService.completeTask(id);
taskUI.render();
};
window.deleteTask = (id: string) => {
if (confirm("确定要删除这个任务吗?")) {
taskService.deleteTask(id);
taskUI.render();
}
};
// 初始化
console.log("任务管理器已启动");
编译和运行
# 编译TypeScript
tsc
# 运行
node dist/index.js
这个项目虽然简单,但涵盖了TypeScript的很多核心特性:枚举、接口、类型别名、工具类型(Partial、Omit)、泛型、函数类型等。你可以在此基础上继续扩展,比如添加搜索功能、编辑任务、数据导出等。
第十章 常见陷阱和最佳实践
最后,分享一些实际开发中容易踩的坑,以及如何避免它们。
陷阱一:滥用any
// ❌ 不好:滥用any
function processData(data: any) {
return data.value; // TypeScript不会检查data是否有value属性
}
// ✓ 好:使用unknown,然后做类型检查
function processData(data: unknown) {
if (typeof data === "object" && data !== null && "value" in data) {
return (data as { value: string }).value;
}
throw new Error("Invalid data format");
}
陷阱二:忽略空值检查
// ❌ 不好:不处理null/undefined
function getUserName(user: { name: string } | null): string {
return user.name; // 如果user是null,运行时会崩
}
// ✓ 好:使用可选链和空值合并
function getUserName(user: { name: string } | null): string {
return user?.name ?? "匿名用户";
}
陷阱三:类型断言滥用
// ❌ 不好:盲目断言
const element = document.getElementById("myElement");
const div = element as HTMLDivElement; // 如果元素不是div,运行时会出问题
// ✓ 好:先检查再断言
const element = document.getElementById("myElement");
if (element instanceof HTMLDivElement) {
const div = element; // TypeScript会自动收窄类型
div.style.color = "red";
}
最佳实践一:开启严格模式
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
严格模式虽然会让编译报错更多,但它能帮你发现潜在的问题。一开始会觉得不舒服,但习惯后就离不开了。
最佳实践二:优先使用as const
// ❌ 不好:类型不够精确
const config = {
theme: "dark",
size: "large"
};
// ✓ 好:使用as const,类型会更精确
const config = {
theme: "dark",
size: "large"
} as const;
// config的类型是 { readonly theme: "dark"; readonly size: "large"; }
as const能让类型更加精确,避免不必要的宽泛类型。
最佳实践三:定义明确的公共API
// ✅ 好:导出明确的接口
export interface UserService {
getUser(id: string): Promise<User>;
createUser(input: CreateUserInput): Promise<User>;
updateUser(id: string, input: UpdateUserInput): Promise<User>;
deleteUser(id: string): Promise<void>;
}
// 导出实现
export const userService: UserService = {
// ...实现
};
这样可以清晰地告诉使用者”这个模块能做什么”,也方便后续替换实现。
写在最后
TypeScript的学习曲线确实比JavaScript陡一些,但它的回报是值得的。当你写代码时,编辑器会实时告诉你哪里有问题;当你重构代码时,TypeScript会帮你找出所有受影响的引用;当你接手别人的代码时,类型注解就是最清晰的文档。
记住,TypeScript不是要限制你的自由,而是给你的自由加上一副安全护具。刚开始可能觉得不习惯,但用久了就会发现,没有类型检查的代码反而更让人不安。
如果你刚开始学习TypeScript,我建议:
- 从严格模式开始,不要先开宽松模式再慢慢调
- 多用工具类型,它们能帮你节省大量时间
- 遇到不确定的类型,多查文档,TypeScript的类型系统很丰富
- 不要害怕报错,每个报错都是在帮你避免潜在的bug
祝你TypeScript之路越走越顺!如果有任何问题,随时可以来问我。编程这条路,本来就是边学边走的,重要的是保持好奇,保持动手。加油!