TS 实战
1 泛型基础与进阶
TS 泛型是什么
泛型函数
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
let value: any;
value = 42;
value = "hello";
console.log(value.toUpperCase()); // 编译不报错,但运行时可能出错
let value: unknown;
value = 42;
value = "hello";
// 不能直接调用字符串方法,必须进行类型检查
if (typeof value === "string") {
console.log(value.toUpperCase()); // 正常运行
}
function throwError(): never {
throw new Error("An error occurred");
}
function infiniteLoop(): never {
while (true) { }
}
总结: any 灵活但不安全;unknown 适合不确定类型但强制类型检查;never 用于不可能有值的场景。
类型守卫
typeof、instanceof、in、自定义类型谓词 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
- 在团队中选择个别优秀的成员开始实践,或自己先实践,不要着急全团队推广。在自己还不熟练时,全团队推广容易让个人形象受损。
- 先在单独某个需求中试点,确保不影响整个团队的开发进度。
- 在试用阶段沉淀好使用经验之后,再全团队推广。
约定规范、避免类型体操
- 不使用 JS 标准中不支持的语法,例如枚举、public、private、重载等
- 尽量直接完整写类型,少使用 extends、交叉类型、联合类型等组合方式生成类型
- 尽量利用类型推导来约定类型
- 暂时放弃或简化对参数为函数的类型约定(涉及逆变与协变,理解困难,可用 any 代替)
- 允许使用
@ts-ignore,学会通过这种方式中断/改变三方库的类型推导 - 在合适的地方,强制使用
as简化类型
这些规范在还不熟练时建议定义,用久了若项目场景没变化,也可以一直用下去。