编程语言

TypeScript教程(二)类型系统进阶

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. 练习

  1. 为项目场景定义 Message 可辨识联合(email/sms/webhook),写一个 estimateCost(msg) 函数,确保有穷尽检查。
  2. 基于 Post 用工具类型生成 PostCreate、PostUpdate,写 updatePost(id, patch: PostUpdate)。
  3. 实现 groupBy<T, K extends PropertyKey>(arr: T[], fn: (x: T) => K): Record<K, T[]>。

文章评论 (0)