TS 实战

高频版 · 来自面试实战和个人笔记

1 泛型基础与进阶

TS 泛型是什么
TypeScript 的泛型(Generics)是一种能够在定义函数、类或接口时,不预先指定具体类型,而是在使用时再指定类型的一种特性。泛型允许你编写更加灵活和可重用的代码,因为你可以将类型参数化,而不局限于某一种具体的类型。

泛型函数

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

let output1 = identity<string>("Hello World"); // 指定 T 为 string
let output2 = identity<number>(100); // 指定 T 为 number

泛型接口

interface GenericIdentityFn<T> {
    (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;

泛型类

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = (x, y) => x + y;
泛型约束 extends
有时你可能想要在泛型中添加一些约束,使得泛型参数不仅仅是「任意类型」。
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

loggingIdentity({ length: 10, value: "Hello World" }); // 有 length 属性,所以可以通过约束

在这个例子中,T 被约束为 Lengthwise 接口,因此只有包含 length 属性的类型才能作为参数传递给 loggingIdentity 函数。

2 泛型工具类

Partial / Required / Readonly

Partial<T> — 将所有属性变为可选

实现原理:type Partial<T> = { [P in keyof T]?: T[P]; }

interface User {
    id: number;
    name: string;
    email: string;
}

function updateUser(id: number, updates: Partial<User>) {
    // 更新逻辑
}
updateUser(1, { name: '新名字' });

Required<T> — 将所有属性设为必需

实现原理:type Required<T> = { [P in keyof T]-?: T[P]; }

interface Props {
    a?: number;
    b?: string;
}
const obj: Required<Props> = {
    a: 1,
    b: '必需的属性',
};

Readonly<T> — 将所有属性设为只读

实现原理:type Readonly<T> = { readonly [P in keyof T]: T[P]; }

interface Config {
    apiKey: string;
    secret: string;
}
const config: Readonly<Config> = {
    apiKey: '12345',
    secret: 'abcde',
};
// config.apiKey = '新的值'; // 编译错误
Pick / Omit / Record

Pick<T, K> — 从 T 中选择属性 K 构造新类型

实现原理:type Pick<T, K extends keyof T> = { [P in K]: T[P]; }

interface Todo {
    title: string;
    description: string;
    completed: boolean;
}
type TodoPreview = Pick<Todo, 'title' | 'completed'>;

Omit<T, K> — 从 T 中剔除属性 K

实现原理:type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

type TodoWithoutDescription = Omit<Todo, 'description'>;

Record<K, T> — 构造键为 K、值为 T 的对象类型

实现原理:type Record<K extends keyof any, T> = { [P in K]: T; }

type Page = 'home' | 'about' | 'contact';
interface PageInfo { title: string; content: string; }
const websitePages: Record<Page, PageInfo> = {
    home: { title: '首页', content: '欢迎来到首页' },
    about: { title: '关于我们', content: '关于我们的内容' },
    contact: { title: '联系我们', content: '联系方式信息' },
};
Exclude / Extract

Exclude<T, U> — 从 T 中排除可赋值给 U 的类型

实现原理:type Exclude<T, U> = T extends U ? never : T;

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<string | number | (() => void), Function>; // string | number

Extract<T, U> — 从 T 中提取可赋值给 U 的类型

实现原理:type Extract<T, U> = T extends U ? T : never;

type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () => void
ReturnType / Parameters

ReturnType<T> — 获取函数返回类型

实现原理:type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function getUser() {
    return { name: 'Alice', age: 30 };
}
type User = ReturnType<typeof getUser>; // { name: string; age: number; }

Parameters<T> — 获取函数参数类型元组

实现原理:type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

function foo(a: string, b: number): void {}
type FooParams = Parameters<typeof foo>; // [string, number]
const params: FooParams = ['hello', 42];
foo(...params);

3 重要类型辨析

any vs unknown vs never
any:表示任意类型,关闭 TypeScript 的类型检查。可以对 any 类型的变量进行任何操作,无需类型检查。是最不安全的类型,应尽量避免。
let value: any;
value = 42;
value = "hello";
console.log(value.toUpperCase());  // 编译不报错,但运行时可能出错
unknown:表示任意类型,但需要进行类型检查后才能进行操作。强制要求开发者在使用前进行显式的类型判断,提供更多类型安全。
let value: unknown;
value = 42;
value = "hello";
// 不能直接调用字符串方法,必须进行类型检查
if (typeof value === "string") {
  console.log(value.toUpperCase());  // 正常运行
}
never:表示永远不会有值的类型,通常用于抛出错误或无法正常结束的函数。
function throwError(): never {
  throw new Error("An error occurred");
}

function infiniteLoop(): never {
  while (true) { }
}

总结: any 灵活但不安全;unknown 适合不确定类型但强制类型检查;never 用于不可能有值的场景。

类型守卫
类型守卫用于在运行时 narrowing 类型,让 TypeScript 在分支内推断出更精确的类型。常见方式:typeofinstanceofin、自定义类型谓词 is
// typeof 守卫
if (typeof value === "string") { /* value 为 string */ }

// 自定义类型谓词
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

4 TS 推行策略

如何在团队中推行 TS
推广方式一定要范围可控。由于 TS 的特殊性,可以先小范围推行,而不是直接在整个大项目中使用。
  • 在团队中选择个别优秀的成员开始实践,或自己先实践,不要着急全团队推广。在自己还不熟练时,全团队推广容易让个人形象受损。
  • 先在单独某个需求中试点,确保不影响整个团队的开发进度。
  • 在试用阶段沉淀好使用经验之后,再全团队推广。
约定规范、避免类型体操
若项目不对外开放,可约定更多规范来简化 TS 使用难度,避免类型体操。
  • 不使用 JS 标准中不支持的语法,例如枚举、public、private、重载等
  • 尽量直接完整写类型,少使用 extends、交叉类型、联合类型等组合方式生成类型
  • 尽量利用类型推导来约定类型
  • 暂时放弃或简化对参数为函数的类型约定(涉及逆变与协变,理解困难,可用 any 代替)
  • 允许使用 @ts-ignore,学会通过这种方式中断/改变三方库的类型推导
  • 在合适的地方,强制使用 as 简化类型

这些规范在还不熟练时建议定义,用久了若项目场景没变化,也可以一直用下去。