Damnatiox
DOCUMENT / published

TypeScript 综合项目:可靠的配置加载器与类型安全客户端

TypeScript 综合项目:可靠的配置加载器与类型安全客户端 本项目把环境、 unknown 解析、可辨识联合、泛型、异步、取消、测试和构建串成完整链路。重点是边界正确,不是堆框架。 1. 目标 实现一个小型 API 客户端: 从环境变量加载并验证配置。 请求 /users/:id ,对 HTTP 与数据错误分类。 支持超时和调用方取消。 不把 any 暴露到领域层。 有运行测试、类型检查和构建门禁。 2. 领域模型与解析器 3.

TypeScript语言基础 2026/8/267 分钟阅读
# 语言基础# TypeScript# TypeScript语言基础

TypeScript 综合项目:可靠的配置加载器与类型安全客户端

本项目把环境、unknown 解析、可辨识联合、泛型、异步、取消、测试和构建串成完整链路。重点是边界正确,不是堆框架。

1. 目标

实现一个小型 API 客户端:

  • 从环境变量加载并验证配置。
  • 请求 /users/:id,对 HTTP 与数据错误分类。
  • 支持超时和调用方取消。
  • 不把 any 暴露到领域层。
  • 有运行测试、类型检查和构建门禁。

2. 领域模型与解析器

TypeScript
export type User = { readonly id: string readonly name: string readonly roles: readonly string[] } function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null } export function parseUser(input: unknown): User { if (!isRecord(input)) throw new TypeError("user must be an object") if (typeof input.id !== "string") throw new TypeError("user.id must be string") if (typeof input.name !== "string") throw new TypeError("user.name must be string") if (!Array.isArray(input.roles) || !input.roles.every((x) => typeof x === "string")) { throw new TypeError("user.roles must be string[]") } return { id: input.id, name: input.name, roles: [...input.roles] } }

3. 配置解析

TypeScript
export type Config = { readonly baseUrl: URL readonly timeoutMs: number } export function loadConfig(env: Readonly<Record<string, string | undefined>>): Config { const rawUrl = env.API_BASE_URL if (rawUrl === undefined) throw new Error("API_BASE_URL is required") const baseUrl = new URL(rawUrl) const timeoutMs = Number(env.API_TIMEOUT_MS ?? "5000") if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) { throw new Error("API_TIMEOUT_MS must be a positive integer") } return { baseUrl, timeoutMs } }

env 作为参数而非直接读取全局,使测试可控。

4. 错误模型

TypeScript
export type ClientError = | { kind: "http"; status: number; body: string } | { kind: "invalid-data"; cause: Error } | { kind: "network"; cause: unknown } export type Result<T> = | { ok: true; value: T } | { ok: false; error: ClientError }

5. 泛型请求执行器

TypeScript
export async function getJson<T>( url: URL, parse: (input: unknown) => T, signal: AbortSignal, ): Promise<Result<T>> { try { const response = await fetch(url, { signal }) if (!response.ok) { return { ok: false, error: { kind: "http", status: response.status, body: await response.text() }, } } try { return { ok: true, value: parse(await response.json()) } } catch (cause) { return { ok: false, error: { kind: "invalid-data", cause: cause instanceof Error ? cause : new Error(String(cause)), }, } } } catch (cause) { return { ok: false, error: { kind: "network", cause } } } }

泛型参数 T 与解析器输出关联;执行器本身不声称 JSON 天然是 T

6. 客户端封装

TypeScript
export class UserClient { constructor(private readonly config: Config) {} async find(id: string, callerSignal?: AbortSignal): Promise<Result<User>> { const timeoutSignal = AbortSignal.timeout(this.config.timeoutMs) const signal = callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal const url = new URL(`/users/${encodeURIComponent(id)}`, this.config.baseUrl) return getJson(url, parseUser, signal) } }

AbortSignal.timeout/any 需要目标运行时支持;若最低版本不支持,应提供已测试的组合辅助函数或 polyfill,而不是只靠 lib 声明。

7. 测试重点

TypeScript
import { describe, expect, it } from "vitest" describe("parseUser", () => { it("returns an isolated validated value", () => { const raw = { id: "u-1", name: "Ada", roles: ["admin"] } const user = parseUser(raw) raw.roles.push("editor") expect(user.roles).toEqual(["admin"]) }) it("rejects invalid roles", () => { expect(() => parseUser({ id: "u-1", name: "Ada", roles: [1] })).toThrow() }) })

集成测试还应覆盖 2xx 非 JSON、4xx/5xx、连接失败、超时、调用方取消和并发请求。

8. 完成标准

  • npm ci && npm run typecheck && npm test && npm run build 通过。
  • 源码没有未说明的 any 或双重断言。
  • 所有外部数据从 unknown 解析。
  • 错误联合在调用方被穷尽处理。
  • 模块配置与真实 Node/构建器一致。
  • 最低运行时版本对 fetchAbortSignal.timeout/any 有验证。
  • 生成 JavaScript 与声明文件已人工抽查。

9. 进一步扩展

加入重试时只对明确的瞬时错误重试,采用指数退避与抖动,并保证请求幂等;加入缓存时把 key、TTL、并发请求合并和失败缓存策略写成显式契约。这些是运行时可靠性问题,类型只能帮助表达状态,不能自动实现策略。