TypeScript 02:类型系统进阶
TS 的核心价值在于类型系统。它是结构化类型(只看形状,不看名字),这点像 Python 的 Protocol,不像 C# 的名义类型。
1. type 与 interface
interface Post {
id: number;
title: string;
content?: string; // 可选
readonly createdAt: Date; // 只读
}
type Comment = {
id: number;
postId: number;
body: string;
};
// interface 可以继承
interface FeaturedPost extends Post {
rank: number;
}
// type 可以用交叉类型组合
type PostWithComments = Post & { comments: Comment[] };
经验:描述对象形状两者都行;需要联合类型、工具类型组合时用
type。团队统一即可。
结构化类型示例
interface Named { name: string }
function hello(x: Named) { console.log(x.name); }
const dog = { name: "Lucky", age: 3 };
hello(dog); // OK:只要"形状"满足即可,不需要显式 implements
2. 联合类型与字面量类型(最常用的特性之一)
type Status = "draft" | "published" | "archived"; // ≈ Python Literal / C# enum
type Id = number | string;
function setStatus(s: Status) { }
setStatus("published"); // OK
// setStatus("deleted"); // 编译错误
3. 类型收窄(Narrowing)
function format(id: number | string): string {
if (typeof id === "number") {
return id.toFixed(0); // 这里 id 被收窄为 number
}
return id.toUpperCase(); // 这里是 string
}
可辨识联合(Discriminated Union)—— 应用场景:处理不同类型的消息/事件
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string; code: number };
function handle(result: ApiResult<Post>) {
if (result.ok) {
console.log(result.data.title); // 自动知道有 data
} else {
console.error(result.code, result.error);
}
}
// 事件处理:类似 Python 3.10 的 match-case
type Event =
| { type: "email"; to: string; subject: string }
| { type: "sms"; phone: string; text: string }
| { type: "push"; deviceId: string };
function send(e: Event): string {
switch (e.type) {
case "email": return `Email to ${e.to}`;
case "sms": return `SMS to ${e.phone}`;
case "push": return `Push to ${e.deviceId}`;
default: {
const never: never = e; // 穷尽检查:新增类型忘了处理会编译报错
return never;
}
}
}
4. 泛型(与 C# 几乎相同)
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// 带约束的泛型
function getById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(i => i.id === id);
}
// 泛型类:一个简单的内存缓存
class Cache<K, V> {
private store = new Map<K, { value: V; expireAt: number }>();
set(key: K, value: V, ttlMs = 60_000) {
this.store.set(key, { value, expireAt: Date.now() + ttlMs });
}
get(key: K): V | undefined {
const item = this.store.get(key);
if (!item || item.expireAt < Date.now()) return undefined;
return item.value;
}
}
const postCache = new Cache<number, Post>();
5. 工具类型(Utility Types)—— 实际开发高频
type PostCreate = Omit<Post, "id" | "createdAt">; // 创建时不需要 id
type PostUpdate = Partial<PostCreate>; // 更新时所有字段可选
type PostPreview = Pick<Post, "id" | "title">; // 列表只展示部分字段
type PostMap = Record<number, Post>; // 字典
type FrozenPost = Readonly<Post>;
type Fn = (x: number) => Promise<string>;
type R = Awaited<ReturnType<Fn>>; // string
应用场景:一个 Post 模型派生出 Create/Update/List 三种 DTO,改一处,处处同步,比 C# 写多个 DTO 类省事。
6. keyof、索引类型、映射类型
type PostKeys = keyof Post; // "id" | "title" | "content" | "createdAt"
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(i => i[key]);
}
declare const posts: Post[];
const titles = pluck(posts, "title"); // string[],写错字段名会报错
// 映射类型:把所有字段变成 "校验函数"
type Validators<T> = { [K in keyof T]?: (value: T[K]) => string | null };
const postValidators: Validators<PostCreate> = {
title: v => (v.length < 3 ? "标题至少 3 个字符" : null),
};
7. 类型守卫与 unknown —— 处理外部数据
外部数据(JSON、接口返回)在运行时是不可信的,TS 类型在运行时会被擦除。
function isPost(x: unknown): x is Post {
return typeof x === "object" && x !== null
&& "id" in x && typeof (x as any).id === "number"
&& "title" in x && typeof (x as any).title === "string";
}
const data: unknown = JSON.parse(text);
if (isPost(data)) {
console.log(data.title);
}
实际项目中用 zod 之类的库做运行时校验(≈ Python 的 pydantic),见第 04 篇。
8. 枚举与 as const
enum Role { Admin = "admin", User = "user" } // 和 C# 类似
// 更推荐:as const + 联合类型(无额外运行时代码)
const ROLES = ["admin", "editor", "viewer"] as const;
type Role2 = typeof ROLES[number]; // "admin" | "editor" | "viewer"
9. 应用场景:类型安全的配置读取
interface AppConfig {
port: number;
dbUrl: string;
env: "dev" | "qa" | "prod";
}
function loadConfig(): AppConfig {
const env = process.env.APP_ENV ?? "dev";
if (!["dev", "qa", "prod"].includes(env)) {
throw new Error(`invalid APP_ENV: ${env}`);
}
return {
port: Number(process.env.PORT ?? 3000),
dbUrl: process.env.DB_URL ?? "sqlite://local.db",
env: env as AppConfig["env"],
};
}
10. 练习
- 为项目场景定义
Message可辨识联合(email/sms/webhook),写一个estimateCost(msg)函数,确保有穷尽检查。 - 基于
Post用工具类型生成PostCreate、PostUpdate,写updatePost(id, patch: PostUpdate)。 - 实现
groupBy<T, K extends PropertyKey>(arr: T[], fn: (x: T) => K): Record<K, T[]>。
文章评论 (0)