TypeScript 从入门到实战

204 阅读4分钟

TypeScript 从入门到实战:写给前端与 Node 工程师的高效指南

为什么选择 TypeScript

  • 类型安全: 把错误前移到开发/编译阶段,显著降低线上事故。
  • 更强 IDE 体验: 自动补全、重构、自文档化的 API。
  • 大中型项目利器: 可维护性、可演进性、跨团队协作成本更低。
  • 生态完善: 主流框架/库(React、Vue、Node、Nest)均良好支持。

一、核心概念与快速上手

基本类型与别名

type UserId = string & { readonly brand: unique symbol }; // 品牌化类型(Nominal)

interface User {
  id: UserId;
  name: string;
  age?: number; // 可选属性
  tags: string[];
}
  • type vs interface
    • interface 支持声明合并,适合描述对象结构和公共 API
    • type 更灵活(联合、交叉、条件类型、映射类型)

函数与 this/重载

function greet(name: string): string {
  return `Hello, ${name}`;
}

interface Formatter {
  (input: string): string;
  locale?: string;
}

function len(x: string): number;
function len(x: any[]): number;
function len(x: string | any[]) { return x.length; }

泛型与约束

function identity<T>(value: T): T { return value; }

interface ApiResponse<TData, TError = never> {
  ok: boolean;
  data?: TData;
  error?: TError;
}

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const r = {} as Pick<T, K>;
  keys.forEach(k => { r[k] = obj[k]; });
  return r;
}

控制流收窄(Narrowing)

function print(value: unknown) {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  } else if (Array.isArray(value)) {
    value.forEach(v => console.log(v));
  }
}

联合类型与判别式联合

type LoadState =
  | { type: 'idle' }
  | { type: 'loading' }
  | { type: 'success'; data: string }
  | { type: 'error'; error: Error };

function handle(state: LoadState) {
  switch (state.type) {
    case 'success': return state.data;
    case 'error': return state.error.message;
    default: return null;
  }
}

二、进阶类型:写出“更聪明”的类型

条件类型与推断

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type ElementType<T> = T extends (infer U)[] ? U : T;

映射类型与修饰符

type ReadonlyDeep<T> = {
  readonly [K in keyof T]: T[K] extends object ? ReadonlyDeep<T[K]> : T[K]
};

type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

工具类型(常用)

  • Partial<T>Required<T>Readonly<T>Pick<T, K>Record<K, T>
  • Omit<T, K>NonNullable<T>ReturnType<F>Parameters<F>
  • ThisType<T>(仅影响上下文类型)

三、工程化与 tsconfig 要点

常用配置

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "nodenext",
    "strict": true,
    "moduleResolution": "nodenext",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] },
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true
  },
  "include": ["src"]
}
  • strict: 强烈建议开启(更强的类型保护)
  • module/moduleResolution: Node ESM/CJS 场景建议 nodenext
  • lib: 浏览器项目通常包含 dom;Node 项目可去掉 dom 避免冲突
  • paths/baseUrl: 配合打包器/运行时解析器使用,避免相对路径地狱

监听与增量

  • npx tsc --init 生成配置
  • npx tsc --watch 持续编译
  • 多包仓库用 Project References 提升构建效率

四、与 Node.js、打包器的协作

模块系统

  • CJS: require/module.exports
  • ESM: import/export"type": "module".mjs
  • Node 18+ 推荐 ESM,tsconfig 选 nodenext

运行与调试

  • 纯 TS: tsc 编译后运行 node dist/index.js
  • 开发期:ts-node 或打包器(esbuild/swc/vite)+ 热重载
  • 调试:node --inspect、VSCode launch 配置

打包器注意点

  • 仅转译模式(esbuild/swc)可能跳过类型检查:搭配 tsc --noEmit 做类型检查
  • 路径别名需在打包器侧同步配置(如 Vite 的 resolve.alias

五、常见坑与最佳实践

1) 类型擦除与运行时零开销

  • TS 类型只在编译期存在,发射的 JS 不带类型检查
  • 若需要运行时校验,配合 zod/io-ts 等库

2) 全局名冲突(DOM 的 Node)

  • 浏览器项目自动引入 dom 声明,其中有全局 Node
  • 避免将自定义类命名为 NodeEventError
  • 或:让文件成为模块(任意 export),或在 lib 中去掉 dom

3) any/unknown/never

  • 优先 unknown 替代 any,强制显式收窄
  • never 表示“不可能到达”,常用于穷尽检查
function exhaustive(_: never): never { throw new Error('unreachable'); }

4) 类型泄漏与边界

  • 库导出的类型要稳定:导出 type 而非“原始内部结构”
  • API 边界用 DTO/Schema 固定合同

5) 仅类型导入与消除副作用

import type { Config } from './types'; // 不会引入运行时代码

六、实战范式与模式

结果类型(错误不抛异常)

type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;

async function safeFetchJson<T>(url: string): Promise<Result<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) return { ok: false, error: new Error(res.statusText) };
    return { ok: true, value: await res.json() as T };
  } catch (e) {
    return { ok: false, error: e as Error };
  }
}

API 响应的“数据契约”与校验

import { z } from 'zod';

const UserDto = z.object({
  id: z.string(),
  name: z.string(),
});
type UserDto = z.infer<typeof UserDto>;

深度只读/可变切换

type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> };
type DeepMutable<T> = { -readonly [K in keyof T]: DeepMutable<T[K]> };

七、测试与类型测试

单元测试

  • Jest/Vitest + ts-jest/内置 TS 支持
  • Node ESM 时优先 Vitest

类型级测试(高级)

  • dtslint、expect-type、tsd:确保导出类型契约不被破坏

八、迷你清单(Cheat Sheet)

  • 启动:npm i -D typescript && npx tsc --init
  • 编译:npx tsc -w(需 tsconfig.json
  • 强类型:开启 strict
  • 避冲突:避免全局名(如 Node),或移除 dom
  • 仅转译:打包器转译 + tsc --noEmit 做类型检查
  • 别名:同步配置 tsconfig 与打包器
  • 生产:导出 .d.tsdeclaration: true)保证类型体验

结语

TypeScript 的价值在于“开发期的确定性”和“演进中的安全网”。理解类型擦除、控制流收窄、泛型与工程化配置,配合合理的模式(Result、Schema 校验、类型边界),就能在浏览器与 Node 场景下写出更稳健、可维护的代码。如果你要落地到团队,优先从严格模式、规范的 tsconfig、稳固的类型契约和 CI 中的类型检查做起。