TypeScript 现代编程教程
·
TypeScript 现代编程教程
面向有 JavaScript 基础的开发者,系统学习 TypeScript 类型系统与工程实践。所有示例均为 TypeScript 5.x,可直接运行。
目录
- 为什么需要 TypeScript
- 环境搭建与编译配置
- 基础类型注解
- 接口与类型别名
- 联合类型与交叉类型
- 泛型
- 条件类型与映射类型
- 函数类型进阶
- 类与访问修饰符
- 枚举与字面量
- 类型守卫与类型收窄
- 工具类型(Utility Types)
- 模块与声明文件
- 异步类型(Promise / async)
- 实战:类型安全的 Todo 应用
1. 为什么需要 TypeScript
1.1 JavaScript 的痛点
// 这段 JS 代码在运行时才会暴露问题
function add(a, b) {
return a + b;
}
add(1, 2); // 3
add("1", "2"); // "12" — 静默地做了字符串拼接
add(1, null); // 1 — null 被转为 0,没有报错
TypeScript 在编译阶段就能捕获这些问题:
function add(a: number, b: number): number {
return a + b;
}
add(1, 2); // ✅
// add("1", "2"); // ❌ 编译报错:类型 "string" 不能赋值给 "number"
1.2 核心价值
| 能力 | 说明 |
|---|---|
| 静态类型检查 | 编码时发现类型错误,而非运行时 |
| 智能提示 | IDE 自动补全、参数提示、跳转定义 |
| 重构安全 | 改名、改签名时自动定位所有引用 |
| 自文档化 | 类型定义本身就是最好的文档 |
2. 环境搭建与编译配置
2.1 安装与初始化
npm install -g typescript
tsc --version # 应输出 5.x
# 在项目中初始化
mkdir my-project && cd my-project
tsc --init # 生成 tsconfig.json
2.2 tsconfig.json 核心配置
{
"compilerOptions": {
"target": "ES2022", // 编译目标 JS 版本
"module": "ESNext", // 模块系统
"moduleResolution": "bundler", // 模块解析策略(现代项目用 bundler)
"strict": true, // ⭐ 开启所有严格检查
"noUncheckedIndexedAccess": true,// 索引访问可能为 undefined
"esModuleInterop": true, // 兼容 CommonJS 默认导入
"skipLibCheck": true, // 跳过 .d.ts 类型检查(加速编译)
"forceConsistentCasingInFileNames": true,
"outDir": "./dist", // 编译输出目录
"rootDir": "./src" // 源码目录
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
2.3 运行方式
# 只做类型检查,不产出文件
tsc --noEmit
# 编译并输出 JS
tsc
# watch 模式
tsc --watch
# 开发时直接用 tsx 运行(推荐)
npx tsx src/index.ts
3. 基础类型注解
3.1 原始类型
let isDone: boolean = false;
let count: number = 42;
let name: string = "张三";
let nothing: null = null;
let notDefined: undefined = undefined;
let big: bigint = 100n; // 大整数
let unique: symbol = Symbol("id");
3.2 数组与元组
// 数组:两种写法等价
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"]; // 泛型写法
// 只读数组
const readonlyNums: readonly number[] = [1, 2, 3];
// readonlyNums.push(4); // ❌ 报错
// 元组:固定长度和类型组合
let pair: [string, number] = ["age", 25];
// pair = [25, "age"]; // ❌ 类型顺序不对
// 具名元组(可读性更好)
type ApiResult = [success: boolean, data: string, code: number];
const result: ApiResult = [true, '{"id":1}', 200];
3.3 对象类型
// 直接注解
const user: { name: string; age: number } = {
name: "张三",
age: 25,
};
// 可选属性用 ?
const config: { host: string; port?: number } = {
host: "localhost",
// port 可以省略
};
// readonly 属性
const point: { readonly x: number; readonly y: number } = { x: 10, y: 20 };
// point.x = 30; // ❌ 只读
// 索引签名:允许任意键
const dict: { [key: string]: number } = {
apples: 3,
bananas: 5,
};
3.4 any、unknown、never
// any:放弃类型检查(尽量少用)
let loose: any = 4;
loose = "hello"; // 不报错
loose.foo.bar(); // 也不报错(运行时可能炸)
// unknown:类型安全的 "未知"
let input: unknown = "hello";
// input.toUpperCase(); // ❌ 必须先收窄类型
if (typeof input === "string") {
input.toUpperCase(); // ✅ 收窄后可用
}
// never:永远不会出现的类型
function throwError(msg: string): never {
throw new Error(msg);
}
function infiniteLoop(): never {
while (true) {}
}
3.5 类型断言
// as 语法(推荐)
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
// 尖括号语法(JSX 中不能用)
const canvas2 = <HTMLCanvasElement>document.getElementById("myCanvas");
// 双重断言:先断言为 unknown,再断言为目标类型(危险,谨慎使用)
const value: string = (42 as unknown) as string;
4. 接口与类型别名
4.1 接口(interface)
interface User {
readonly id: number;
name: string;
email: string;
age?: number; // 可选
tags: string[];
}
// 使用
const u1: User = {
id: 1,
name: "张三",
email: "zhang@example.com",
tags: ["admin"],
};
// 扩展接口
interface Admin extends User {
permissions: string[];
level: number;
}
const admin: Admin = {
id: 2,
name: "管理员",
email: "admin@example.com",
tags: ["admin", "super"],
permissions: ["delete", "manage"],
level: 9,
};
4.2 类型别名(type)
// 基本类型别名
type ID = number | string;
type Point = { x: number; y: number };
// 联合类型
type Status = "idle" | "loading" | "success" | "error";
// 函数类型
type Callback = (data: string) => void;
// 交叉类型
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged & { city: string };
const p: Person = { name: "李四", age: 30, city: "北京" };
4.3 interface vs type 区别
// 1. interface 可以声明合并,type 不能
interface Window {
title: string;
}
interface Window {
ts: number; // 自动合并
}
const win: Window = { title: "test", ts: 42 };
// 2. interface 只能描述对象,type 可以描述任意类型
type Name = string; // ✅ type 可以
// interface Name extends string {} // ❌ interface 不行
// 3. type 可以做映射类型
type Nullable<T> = { [K in keyof T]: T[K] | null }; // ✅
// interface 做不到
// 选择策略:描述对象形状优先用 interface,其他场景用 type
5. 联合类型与交叉类型
5.1 联合类型(Union Types)
// 基本联合
type Result = "success" | "error";
type StatusCode = 200 | 201 | 400 | 404 | 500;
// 函数参数接受多种类型
function printId(id: number | string): void {
console.log(`ID: ${id}`);
}
printId(101); // ✅
printId("abc-123"); // ✅
// printId(true); // ❌
// 联合类型需要收窄后才能使用特定方法
function getLength(value: string | string[]): number {
return value.length; // ✅ length 是共同的属性
}
function toUpperCase(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // ✅ 收窄为 string
}
return value.toString(); // ✅ 收窄为 number
}
5.2 可辨识联合(Discriminated Unions)
这是 TypeScript 最强大的模式之一:
// 用公共字段来区分类型
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // shape 收窄为 circle
case "rectangle":
return shape.width * shape.height; // shape 收窄为 rectangle
case "triangle":
return (shape.base * shape.height) / 2; // shape 收窄为 triangle
}
}
// 实际应用:API 响应
type ApiResponse =
| { status: "ok"; data: User[] }
| { status: "error"; code: number; message: string };
function handleResponse(resp: ApiResponse): void {
if (resp.status === "ok") {
console.log(`获取到 ${resp.data.length} 条数据`);
} else {
console.error(`错误 [${resp.code}]: ${resp.message}`);
}
}
5.3 交叉类型(Intersection Types)
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
// 交叉 = 合并所有属性
type ColorfulCircle = Colorful & Circle;
const cc: ColorfulCircle = {
color: "red",
radius: 5,
};
// 函数混合(mixin 模式)
type Point2D = { x: number; y: number };
type WithId = { id: string };
type WithTimestamp = { createdAt: Date; updatedAt: Date };
type GeoPoint = Point2D & WithId & WithTimestamp;
6. 泛型
泛型是 TypeScript 类型系统最核心的抽象能力。
6.1 基本泛型
// 不使用泛型:丢失了类型信息
function first(arr: any[]): any {
return arr[0];
}
const a = first([1, 2, 3]); // a 的类型是 any
// 使用泛型:保留类型
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const a1 = first([1, 2, 3]); // a1: number | undefined
const a2 = first(["a", "b"]); // a2: string | undefined
const a3 = first([]); // a3: undefined
6.2 泛型约束
// extends 约束泛型必须有某些属性
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(`长度: ${item.length}`);
return item;
}
logLength("hello"); // 5
logLength([1, 2, 3]); // 3
logLength({ length: 10 });// 10
// logLength(123); // ❌ number 没有 length 属性
// 用 keyof 约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "张三", age: 25 };
getProperty(user, "name"); // ✅ 返回 string
getProperty(user, "age"); // ✅ 返回 number
// getProperty(user, "email"); // ❌ email 不是 user 的 key
6.3 泛型工具函数实战
// 交换元组元素
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
const swapped = swap([1, "hello"]); // [string, number]
// 安全地合并对象
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
const merged = merge({ name: "张三" }, { age: 25 });
// merged: { name: string } & { age: number }
// 状态机
function createState<T>(initial: T) {
let state: T = initial;
return {
get: (): T => state,
set: (next: T): void => { state = next; },
update: (fn: (prev: T) => T): void => { state = fn(state); },
};
}
const counter = createState(0);
counter.set(10);
counter.update((n) => n + 1);
console.log(counter.get()); // 11
6.4 泛型类
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("hello");
6.5 泛型默认值
// 不传泛型参数时使用默认类型
interface ApiConfig<T = Record<string, unknown>> {
url: string;
data?: T;
timeout?: number;
}
// 简单请求
const simple: ApiConfig = { url: "/api/health" };
// 带类型的请求
interface LoginData {
username: string;
password: string;
}
const login: ApiConfig<LoginData> = {
url: "/api/login",
data: { username: "admin", password: "123456" },
};
7. 条件类型与映射类型
7.1 条件类型
// 语法:T extends U ? X : Y 在类型层面做判断
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<number>; // false
// 实用的条件类型
type NonNullable<T> = T extends null | undefined ? never : T;
type Flatten<T> = T extends (infer U)[] ? U : T;
type S = Flatten<string[]>; // string
type N = Flatten<number>; // number
7.2 infer 关键字
// 提取数组元素类型
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type El = ArrayElement<number[]>; // number
// 提取函数返回类型(TS 内置的 ReturnType 就这样实现)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { name: "张三", age: 25 };
}
type UserType = MyReturnType<typeof getUser>;
// { name: string; age: number }
// 提取 Promise 内部类型
type Awaited<T> = T extends Promise<infer U> ? U : T;
type P = Awaited<Promise<string>>; // string
7.3 映射类型(Mapped Types)
// 把对象所有属性变为可选
type Partial<T> = {
[K in keyof T]?: T[K];
};
// 把对象所有属性变为只读
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// 原始类型
interface User {
name: string;
age: number;
email: string;
}
// 应用
type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string }
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }
// 自定义:把所有属性变为 string 类型
type Stringify<T> = {
[K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { name: string; age: string; email: string }
7.4 模板字面量类型(Template Literal Types)
// Python / Rust 风格的字符串模式匹配
type EventName = `on${Capitalize<string>}`;
// "onClick" | "onChange" | "onSubmit" | ...
// 实际用例:CSS 类名
type Size = "sm" | "md" | "lg";
type Variant = "primary" | "secondary" | "danger";
type ClassName = `btn-${Variant}-${Size}`;
// "btn-primary-sm" | "btn-primary-md" | ... | "btn-danger-lg"
// 路由参数
type Route =
| "/users"
| `/users/${string}`
| `/users/${string}/posts`;
function navigate(path: Route): void {}
navigate("/users"); // ✅
navigate("/users/42"); // ✅
navigate("/users/42/posts"); // ✅
// navigate("/admins"); // ❌
7.5 映射类型修饰符(+ / -)
// 去掉 readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// 去掉可选
type Required<T> = {
[K in keyof T]-?: T[K];
};
interface Config {
readonly host: string;
readonly port?: number;
}
type MutableConfig = Mutable<Config>;
// { host: string; port?: number | undefined }
type RequiredConfig = Required<Config>;
// { readonly host: string; readonly port: number }
8. 函数类型进阶
8.1 函数签名
// 完整的函数类型签名
type AddFn = (a: number, b: number) => number;
// 重载签名(多个签名 + 一个实现)
function format(value: number): string;
function format(value: string): string;
function format(value: boolean): string;
function format(value: number | string | boolean): string {
return `格式化: ${value}`;
}
format(42); // ✅
format("hello"); // ✅
format(true); // ✅
// 泛型重载
function firstElement<T>(arr: T[]): T | undefined;
function firstElement<T>(arr: T, n: number): T;
function firstElement<T>(arr: T[], n?: number): T | T[] | undefined {
if (n !== undefined) return arr[n];
return arr[0];
}
8.2 this 参数
// 声明 this 的类型(第一个参数必须是 this,编译后会被移除)
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCard(this: Deck, suitIndex: number, cardIndex: number): Card;
}
const deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array.from({ length: 52 }, (_, i) => i + 1),
createCard(this: Deck, suitIndex: number, cardIndex: number): Card {
return {
suit: this.suits[suitIndex]!,
card: this.cards[cardIndex]!,
};
},
};
8.3 剩余参数与参数解构
// 剩余参数
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
// 解构参数
function createUser({ name, age }: { name: string; age: number; email?: string }): string {
return `${name}, ${age}岁`;
}
// 解构 + 剩余
interface SearchParams {
keyword: string;
page: number;
sortBy?: string;
}
function search({ keyword, page, ...rest }: SearchParams): void {
console.log(`搜索: ${keyword}, 第${page}页, 其他:`, rest);
}
8.4 不变量断言(as const)
// as const 让 TypeScript 推导出最窄的字面量类型
const colors = ["red", "green", "blue"] as const;
// 类型: readonly ["red", "green", "blue"]
type Color = (typeof colors)[number]; // "red" | "green" | "blue"
const config = {
host: "localhost",
port: 8080,
retry: 3,
} as const;
// 类型: { readonly host: "localhost"; readonly port: 8080; readonly retry: 3 }
// 推导出精确值而非宽泛类型
const roles = {
admin: "ADMIN",
user: "USER",
guest: "GUEST",
} as const;
type Role = (typeof roles)[keyof typeof roles]; // "ADMIN" | "USER" | "GUEST"
9. 类与访问修饰符
9.1 访问修饰符
class Animal {
public name: string; // 公开(默认)
private _id: string; // 私有:仅类内部可访问
protected type: string; // 受保护:类内部 + 子类可访问
readonly species: string; // 只读
constructor(name: string, species: string) {
this.name = name;
this.species = species;
this._id = crypto.randomUUID();
this.type = "animal";
}
// getter / setter
get id(): string {
return this._id.slice(0, 8) + "...";
}
set id(value: string) {
// 通常不提供 setter 或加验证
throw new Error("不能手动设置 ID");
}
}
class Dog extends Animal {
constructor(name: string) {
super(name, "dog");
console.log(this.type); // ✅ 子类可以访问 protected
// console.log(this._id); // ❌ private 不行
}
}
9.2 参数属性(简写构造器)
// 普通写法
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// 参数属性:一行搞定
class UserShort {
constructor(
public name: string,
public age: number,
private email: string,
protected role: string = "user",
readonly id: string = crypto.randomUUID(),
) {}
}
const u = new UserShort("张三", 25, "zhang@example.com");
console.log(u.name); // ✅ public
// console.log(u.email); // ❌ private
9.3 抽象类与接口实现
interface Repository<T> {
getAll(): Promise<T[]>;
getById(id: string): Promise<T | null>;
create(item: Omit<T, "id">): Promise<T>;
}
// 抽象类:部分实现,留给子类实现
abstract class BaseRepository<T extends { id: string }> implements Repository<T> {
protected items: Map<string, T> = new Map();
async getAll(): Promise<T[]> {
return [...this.items.values()];
}
async getById(id: string): Promise<T | null> {
return this.items.get(id) ?? null;
}
// 子类必须实现
abstract create(item: Omit<T, "id">): Promise<T>;
}
// 具体实现
interface Task {
id: string;
title: string;
done: boolean;
}
class TaskRepository extends BaseRepository<Task> {
async create(item: Omit<Task, "id">): Promise<Task> {
const task: Task = { ...item, id: crypto.randomUUID() };
this.items.set(task.id, task);
return task;
}
}
9.4 静态成员
class MathUtils {
static readonly PI = Math.PI;
static clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
static #instanceCount = 0; // 静态私有
static create(): string {
this.#instanceCount++;
return `instance-${this.#instanceCount}`;
}
}
console.log(MathUtils.PI);
console.log(MathUtils.clamp(150, 0, 100)); // 100
console.log(MathUtils.create()); // instance-1
10. 枚举与字面量
10.1 数字枚举与字符串枚举
// 数字枚举:默认从 0 开始自增
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// 字符串枚举:不会自增,每个值必须显式指定
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
}
// 用在哪里
function request(url: string, method: HttpMethod): void {
console.log(`${method} ${url}`);
}
request("/api/users", HttpMethod.GET);
10.2 const enum(编译时内联,零运行时开销)
const enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
}
const level = LogLevel.Info;
// 编译后 JS: const level = 1; 直接内联,没有对象生成
10.3 字面量联合类型 —— 枚举的替代方案
// 很多团队更倾向用字面量联合而非 enum
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type LogLevel = "debug" | "info" | "warn" | "error";
function log(level: LogLevel, message: string): void {
const prefix = `[${level.toUpperCase()}]`;
console.log(`${prefix} ${message}`);
}
log("info", "服务启动"); // ✅
// log("verbose", "..."); // ❌
// 配合 as const + typeof 从对象推导
const LOG_LEVELS = {
DEBUG: "debug",
INFO: "info",
WARN: "warn",
ERROR: "error",
} as const;
type LogLevelFromObject = (typeof LOG_LEVELS)[keyof typeof LOG_LEVELS];
// "debug" | "info" | "warn" | "error"
11. 类型守卫与类型收窄
11.1 typeof 守卫
function process(value: string | number | boolean): string {
if (typeof value === "string") {
return value.toUpperCase(); // value: string
}
if (typeof value === "number") {
return value.toFixed(2); // value: number
}
return value ? "true" : "false"; // value: boolean
}
11.2 instanceof 守卫
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
) {
super(message);
this.name = "ApiError";
}
}
class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = "NetworkError";
}
}
function handleError(err: Error): string {
if (err instanceof ApiError) {
return `API 错误 [${err.statusCode}]: ${err.message}`;
}
if (err instanceof NetworkError) {
return `网络错误: ${err.message}`;
}
return `未知错误: ${err.message}`;
}
11.3 自定义类型守卫(is 关键字)
interface Cat {
meow(): void;
name: string;
}
interface Dog {
bark(): void;
breed: string;
}
type Pet = Cat | Dog;
// 自定义守卫函数:返回 "x is Type"
function isCat(pet: Pet): pet is Cat {
return "meow" in pet;
}
function isDog(pet: Pet): pet is Dog {
return (pet as Dog).bark !== undefined;
}
// 使用
function interact(pet: Pet): void {
if (isCat(pet)) {
pet.meow(); // ✅ pet 收窄为 Cat
} else {
pet.bark(); // ✅ pet 收窄为 Dog
}
}
11.4 in 运算符收窄
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function move(animal: Fish | Bird): void {
if ("swim" in animal) {
animal.swim(); // animal: Fish
} else {
animal.fly(); // animal: Bird
}
}
11.5 断言函数(Asserts)
// asserts 告知 TypeScript 此函数如果正常返回,则条件一定成立
function assert(condition: unknown, message?: string): asserts condition {
if (!condition) {
throw new Error(message ?? "断言失败");
}
}
function getConfig(): { port: number } | undefined {
return Math.random() > 0.5 ? { port: 8080 } : undefined;
}
const config = getConfig();
assert(config !== undefined, "配置不能为空");
console.log(config.port); // ✅ config 现在一定不是 undefined
// 如果没有 assert,上面这行会报错 "config 可能为 undefined"
11.6 satisfies 运算符(TypeScript 4.9+)
// satisfies: 检查值是否符合类型,但保留最窄的推导类型
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Record<string, string | number[]>;
// ↑ 检查每个值都是 string 或 number[]
// palette.red 的类型是 [255, 0, 0] — 保留了元组,没有被拓宽为 (string | number[])[]
palette.red[0]; // ✅ 255
// 不用 satisfies 的话:
// const palette: Record<string, string | number[]> = { ... }
// palette.red[0] // ❌ 报错,因为 red 被拓宽为 string | number[]
12. 工具类型(Utility Types)
TypeScript 内置了大量实用工具类型。
12.1 对象操作
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Partial<T> — 全部可选
type UpdateUser = Partial<User>;
// Required<T> — 全部必填
type StrictUser = Required<User>;
// Pick<T, K> — 选取部分字段
type UserPublic = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }
// Omit<T, K> — 排除部分字段
type UserWithoutPassword = Omit<User, "password">;
// { id: number; name: string; email: string; createdAt: Date }
// Readonly<T> — 全部只读
type ReadonlyUser = Readonly<User>;
12.2 记录与排除
// Record<K, V> — 构造键值对对象
type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;
const perms: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
// Exclude<T, U> — 从联合中排除
type NonAdminRoles = Exclude<Role, "admin">; // "user" | "guest"
// Extract<T, U> — 从联合中提取
type OnlyLoading = Extract<"idle" | "loading" | "success", "loading">; // "loading"
12.3 函数类型工具
// Parameters<T> — 提取函数参数类型
function login(username: string, password: string, remember: boolean): void {}
type LoginParams = Parameters<typeof login>;
// [username: string, password: string, remember: boolean]
// ReturnType<T> — 提取函数返回类型
function getUser() {
return { name: "张三", age: 25, role: "admin" as const };
}
type UserFromFn = ReturnType<typeof getUser>;
// { name: string; age: number; role: "admin" }
// Awaited<T> — 提取 Promise 内部类型
type UserPromise = Promise<{ name: string }>;
type ResolvedUser = Awaited<UserPromise>; // { name: string }
12.4 字符串操作(TypeScript 4.1+)
// 内置的字符串操作类型
type EventType = "mouseclick" | "keydown" | "formsubmit";
// Uppercase / Lowercase / Capitalize / Uncapitalize
type UpperEvent = Uppercase<EventType>;
// "MOUSECLICK" | "KEYDOWN" | "FORMSUBMIT"
type CapitalizedEvent = Capitalize<EventType>;
// "Mouseclick" | "Keydown" | "Formsubmit"
// 组合使用
type EventHandler = `on${Capitalize<EventType>}`;
// "onMouseclick" | "onKeydown" | "onFormsubmit"
12.5 NonNullable
// 从联合类型中排除 null 和 undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
// 常用场景:过滤数组中的空值
const items: (string | null | undefined)[] = ["a", null, "b", undefined, "c"];
const safeItems: string[] = items.filter((x): x is string => x != null);
13. 模块与声明文件
13.1 ES 模块
// math.ts — 导出
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14159;
export type Vector2D = { x: number; y: number };
export default class Calculator {
add(a: number, b: number): number { return a + b; }
}
// app.ts — 导入
import Calculator, { add, PI, type Vector2D } from "./math";
const calc = new Calculator();
console.log(add(1, 2));
// 只导入类型(编译后被完全移除)
import type { Vector2D } from "./math";
13.2 声明文件(.d.ts)
// types.d.ts — 全局类型声明文件
// 声明一个没有类型定义的 npm 模块
declare module "my-untyped-lib" {
export function doSomething(input: string): number;
export const version: string;
}
// 扩展全局变量
declare global {
interface Window {
__APP_VERSION__: string;
__INITIAL_STATE__: Record<string, unknown>;
}
// 声明全局函数
function gtag(event: string, params: Record<string, unknown>): void;
}
export {}; // 确保这是一个模块文件
13.3 声明第三方库的类型
// 给已有的 JS 库补充类型声明
// react-query 的类型声明示例
declare module "@tanstack/react-query" {
export function useQuery<TData>(
key: string[],
fetcher: () => Promise<TData>,
options?: {
enabled?: boolean;
staleTime?: number;
retry?: number;
}
): {
data: TData | undefined;
isLoading: boolean;
isError: boolean;
error: Error | null;
};
}
14. 异步类型(Promise / async)
14.1 Promise 泛型
// Promise<T> — T 是 resolve 的值的类型
function fetchUser(id: number): Promise<{ name: string; age: number }> {
return fetch(`/api/users/${id}`).then((res) => res.json());
}
// async 函数自动包裹返回值为 Promise
async function getUserName(id: number): Promise<string> {
const user = await fetchUser(id);
return user.name;
}
14.2 带错误处理的类型
// 定义 API 响应的联合类型
type Result<T> =
| { ok: true; data: T }
| { ok: false; error: string; code: number };
async function safeFetch<T>(url: string): Promise<Result<T>> {
try {
const res = await fetch(url);
if (!res.ok) {
return { ok: false, error: res.statusText, code: res.status };
}
const data: T = await res.json();
return { ok: true, data };
} catch (e) {
return {
ok: false,
error: e instanceof Error ? e.message : "未知错误",
code: 0,
};
}
}
// 使用(类型安全,不会漏处理错误)
const result = await safeFetch<User[]>("/api/users");
if (result.ok) {
result.data.forEach((user) => console.log(user.name));
} else {
console.error(`请求失败 [${result.code}]: ${result.error}`);
}
14.3 AbortController 的类型安全封装
interface FetchOptions extends RequestInit {
timeout?: number;
}
async function fetchWithTimeout(
url: string,
options: FetchOptions = {}
): Promise<Response> {
const { timeout = 10000, ...fetchOptions } = options;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
});
return response;
} finally {
clearTimeout(timer);
}
}
15. 实战:类型安全的 Todo 应用
这是一个完整的 TypeScript 实战项目,涵盖了本教程的核心知识点。
// ==================== 类型定义 ====================
type Priority = "low" | "medium" | "high";
type TodoStatus = "todo" | "in-progress" | "done";
interface Todo {
readonly id: string;
title: string;
description: string;
status: TodoStatus;
priority: Priority;
tags: string[];
createdAt: Date;
updatedAt: Date;
}
// 创建和更新用的 DTO(Data Transfer Object)
type CreateTodoInput = Pick<Todo, "title" | "description" | "priority" | "tags">;
type UpdateTodoInput = Partial<Pick<Todo, "title" | "description" | "status" | "priority" | "tags">>;
// 查询过滤
interface TodoFilter {
status?: TodoStatus;
priority?: Priority;
tag?: string;
keyword?: string;
}
// ==================== 数据层 ====================
class TodoStore {
private todos: Map<string, Todo> = new Map();
create(input: CreateTodoInput): Todo {
const now = new Date();
const todo: Todo = {
...input,
id: crypto.randomUUID(),
status: "todo",
createdAt: now,
updatedAt: now,
};
this.todos.set(todo.id, todo);
return todo;
}
getById(id: string): Todo | undefined {
return this.todos.get(id);
}
update(id: string, input: UpdateTodoInput): Todo | null {
const existing = this.todos.get(id);
if (!existing) return null;
const updated: Todo = {
...existing,
...input,
updatedAt: new Date(),
};
this.todos.set(id, updated);
return updated;
}
delete(id: string): boolean {
return this.todos.delete(id);
}
getAll(filter?: TodoFilter): Todo[] {
let items = [...this.todos.values()];
if (filter?.status) {
items = items.filter((t) => t.status === filter.status);
}
if (filter?.priority) {
items = items.filter((t) => t.priority === filter.priority);
}
if (filter?.tag) {
items = items.filter((t) => t.tags.includes(filter.tag!));
}
if (filter?.keyword) {
const kw = filter.keyword.toLowerCase();
items = items.filter(
(t) =>
t.title.toLowerCase().includes(kw) ||
t.description.toLowerCase().includes(kw)
);
}
return items.sort(
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
);
}
getStats(): { total: number; done: number; byPriority: Record<Priority, number> } {
const all = [...this.todos.values()];
return {
total: all.length,
done: all.filter((t) => t.status === "done").length,
byPriority: {
low: all.filter((t) => t.priority === "low").length,
medium: all.filter((t) => t.priority === "medium").length,
high: all.filter((t) => t.priority === "high").length,
},
};
}
}
// ==================== 工具函数 ====================
const PRIORITY_LABELS: Record<Priority, string> = {
low: "🟢 低",
medium: "🟡 中",
high: "🔴 高",
};
const STATUS_LABELS: Record<TodoStatus, string> = {
"todo": "📋 待办",
"in-progress": "🔄 进行中",
"done": "✅ 完成",
};
function formatTodo(todo: Todo): string {
const lines = [
`[${todo.id.slice(0, 8)}] ${todo.title}`,
` 优先级: ${PRIORITY_LABELS[todo.priority]} | 状态: ${STATUS_LABELS[todo.status]}`,
` 描述: ${todo.description || "(无)"}`,
` 标签: ${todo.tags.length > 0 ? todo.tags.join(", ") : "(无)"}`,
` 创建: ${todo.createdAt.toLocaleString()}`,
];
return lines.join("\n");
}
// ==================== 使用示例 ====================
// 单例
const store = new TodoStore();
// 创建
const todo1 = store.create({
title: "学习 TypeScript 泛型",
description: "完成泛型章节的练习",
priority: "high",
tags: ["学习", "TypeScript"],
});
store.create({
title: "写周报",
description: "总结本周工作",
priority: "medium",
tags: ["工作"],
});
store.create({
title: "跑步",
description: "3公里",
priority: "low",
tags: ["运动"],
});
// 更新
store.update(todo1.id, { status: "in-progress" });
// 查询
console.log("--- 高优先级任务 ---");
store.getAll({ priority: "high" }).forEach((t) => console.log(formatTodo(t)));
console.log("\n--- 所有任务 ---");
store.getAll().forEach((t) => console.log(formatTodo(t)));
// 统计
const stats = store.getStats();
console.log("\n--- 统计 ---");
console.log(`总数: ${stats.total}, 完成: ${stats.done}`);
console.log(`低/中/高: ${stats.byPriority.low}/${stats.byPriority.medium}/${stats.byPriority.high}`);
继续学习路线
- zod — 运行时数据校验,生成 TypeScript 类型
- ts-pattern — 库级别的模式匹配,比 switch 更强大
- @tanstack/react-query — 服务端状态管理的类型实践
- Zustand — 极致简洁的状态管理,TS 体验一流
- tRPC — 端到端类型安全的 API 方案
- Effect-TS — 函数式编程 + 类型级错误处理
- type-challenges — GitHub 上的 TS 类型体操练习题
更多推荐
所有评论(0)