哈喽大家好,我是Lotzinfly,一位前端小猎人。欢迎大家来到前端丛林,在这里你将会遇到各种各样的前端猎物,我希望可以把这些前端猎物统统拿下,嚼碎了服用,并成为自己身上的骨肉。当我们学习前端进阶的时候,TypeScript是重中之重。学会TypeScript将大大提高我们的开发效率,所以一定要掌握好TypeScript。
这篇文章让我们手写12个TypeScript奇淫技巧,并逐行分析,最近秋招就要来了,学会手写TypeScript例子就可以大胆和面试官畅所欲言和自信谈薪啦!😏😏😏 击败TypeScript并嚼碎了服用将会给我们带来大量经验,你们准备好了吗?话不多说,现在开启我们今天的前端丛林冒险之旅吧!
1. Partial 深度递归版
场景 |
更新一个嵌套对象,但只想传"任意深度"的片段 |
易错 |
原生 Partial 只擦一层,{ address: { city: undefined } } 会把外层 address 整个覆盖 |
进化 |
手写 DeepPartial ,同时保留"数组"结构 |
ts
复制代码
// 工具
type DeepPartial<T> = T extends object
? T extends readonly any[]
? ReadonlyArray<DeepPartial<T[number]>>
: { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// domain
interface User {
id: number;
profile: {
name: string;
avatar: { url: string; size: [number, number] };
};
tags: string[];
}
// 使用:只改头像宽度
const patch: DeepPartial<User> = {
profile: { avatar: { size: [undefined, 128] } },
};
// 单元测试
function testDeepPartial() {
const original: User = {
id: 1,
profile: { name: 'Tom', avatar: { url: 'a.png', size: [64, 64] } },
tags: ['vip'],
};
const merged = merge(original, patch); // 手写 merge 或 lodash.merge
console.assert(merged.profile.avatar.size[0] === 64); // 未被覆盖
console.assert(merged.profile.avatar.size[1] === 128);
}
2. Required 带"运行时检查"
场景 |
后端返沪配置字段全部可选,但前端初始化时必须补齐 |
进化 |
把 Required 丢给 zod,自动生成"运行时校验 + 类型" |
ts
复制代码
import { z } from 'zod';
const ConfigSchema = z.object({
apiUrl: z.string().url(),
timeout: z.number().int().positive(),
retry: z.boolean(),
});
// 推导后全部是必填
type Config = z.infer<typeof ConfigSchema>; // Required<{apiUrl:string; timeout:number; retry:boolean}>
// 使用:本地 config.json 可能缺字段
function loadConfig(): Config {
const raw = JSON.parse(readFileSync('./config.json', 'utf-8'));
return ConfigSchema.parse(raw); // 缺字段会直接抛错
}
3. Readonly 冻结"React 状态"
场景 |
把 Redux Toolkit 的 state 彻底锁死,防止误赋值 |
进化 |
结合 immer 的 Draft 类型,做到"写时复制、读时只读" |
ts
复制代码
import type { Draft } from 'immer';
interface State {
user: { name: string };
list: number[];
}
const initialState: Readonly<State> = {
user: { name: 'Alice' },
list: [1, 2, 3],
};
// 在 reducer 里只能改 Draft,外面拿到的是 Readonly
function reducer(state: Readonly<State>, action: any) {
return produce(state, (draft: Draft<State>) => {
draft.list.push(4); // ✅ 允许
});
}
// 组件里
const state: Readonly<State> = useSelector(s => s);
state.list.push(5); // ❌ TS 报错:Property 'push' does not exist on readonly number[]
4. Pick<T,K> 造"最小列"数据库查询
场景 |
ORM 只查需要用到的列,减少网络传输 |
进化 |
把 Pick 做成"通用返回类型",再配合 sql tag |
ts
复制代码
// 通用工具:指定列查询
type PickCol<T, K extends keyof T> = Pick<T, K>;
interface UserTable {
id: number;
name: string;
password: string;
createdAt: Date;
}
// 只查两列
async function getUserNames(): Promise<PickCol<UserTable, 'id' | 'name'>[]> {
return sql<{ id: number; name: string }[]>`SELECT id, name FROM user`;
}
// 调用方永远拿不到 password
const list = await getUserNames();
list[0].password; // ❌ TS 报错
5. Omit<T,K> 造"公有类型"同时保持单一代码源
场景 |
同一个 User 实体,内部服务需要 password ,开放给前端的 DTO 不需要 |
进化 |
用 Omit 生成 PublicUser,但不手写第二份接口,保证"字段新增/删除"时两边同步 |
ts
复制代码
interface User {
id: number;
name: string;
password: string;
internalRemark: string;
}
// 所有对外接口都用 PublicUser
export type PublicUser = Omit<User, 'password' | 'internalRemark'>;
// 新增字段 email 时,只改 User,PublicUser 自动同步
6. Record<K,T> 做"字典"时给 value 加"默认值函数"
场景 |
权限字典初始化时,希望缺失 key 自动兜底 |
进化 |
封装 getWithDefault 高阶函数,保持 Record 类型安全 |
ts
复制代码
type Role = 'admin' | 'user' | 'guest';
const defaultPerms = {
admin: { read: true, write: true },
user: { read: true, write: false },
guest: { read: false, write: false },
} satisfies Record<Role, { read: boolean; write: boolean }>;
function getPerm<R extends Role>(r: R): Record<Role, typeof defaultPerms[R]> {
return defaultPerms[r] ?? defaultPerms.guest;
}
// 使用
const p = getPerm('user');
p.write; // false
场景 |
一个组件只接受部分颜色,需要把系统级颜色过滤出来 |
ts
复制代码
type SystemColor = 'red' | 'green' | 'blue' | 'yellow';
type AllowedColor = Extract<SystemColor, 'red' | 'green'>; // 'red' | 'green'
type DisallowedColor = Exclude<SystemColor, AllowedColor>; // 'blue' | 'yellow'
// 再进一步做运行时 map 检查
const COLOR_MAP: Record<AllowedColor, string> = {
red: '#ff0000',
green: '#00ff00',
};
function setColor(c: AllowedColor) {
document.body.style.color = COLOR_MAP[c];
}
8. NonNullable 与"空值断言"组合拳
场景 |
数组查找后立刻使用,不想写一堆 if |
进化 |
封装"查找 or 抛"函数,返回 NonNullable<T> |
ts
复制代码
function findStrict<T, K extends keyof T>(
arr: T[],
key: K,
value: T[K]
): NonNullable<T> {
const item = arr.find(v => v[key] === value);
if (!item) throw new Error(`Not found`);
return item!; // ! 断言后类型擦除 null|undefined
}
// 使用
const users: ({ id: number; name: string } | null)[] = [
{ id: 1, name: 'A' },
null,
];
const u = findStrict(users, 'id', 1); // 返回类型 {id:number; name:string},null 被排除
9. ReturnType + Parameters 做"函数装饰器"不改类型
场景 |
给任意 async 函数加"自动重试"包裹,但保持原类型 |
ts
复制代码
function withRetry<F extends (...args: any[]) => Promise<any>>(fn: F) {
return async (...args: Parameters<F>): Promise<ReturnType<F>> => {
let lastErr: unknown;
for (let i = 0; i < 3; i++) {
try {
return (await fn(...args)) as ReturnType<F>;
} catch (e) {
lastErr = e;
await new Promise(r => setTimeout(r, 100 * 2 ** i));
}
}
throw lastErr;
};
}
// 使用:类型完全不变
const fetchUser = async (id: number) => ({ id, name: 'A' });
const safeFetch = withRetry(fetchUser);
// safeFetch 签名仍是 (id:number)=>Promise<{id:number; name:string}>
10. ConstructorParameters + InstanceType 造"通用工厂"
场景 |
写 DI 容器,需要根据"类"自动产生工厂函数 |
ts
复制代码
class Logger {
constructor(public prefix: string) {}
log(msg: string) {
console.log(`[${this.prefix}] ${msg}`);
}
}
// 通用工厂
function createFactory<T extends abstract new (...args: any) => any>(Cls: T) {
return (...args: ConstructorParameters<T>): InstanceType<T> =>
new Cls(...args);
}
// 使用
const makeLogger = createFactory(Logger);
const log = makeLogger('App'); // 类型自动推导为 Logger
log.log('started');
11. ThisParameterType 给"方法链"锁上下文
场景 |
jQuery 风格链式调用,确保 this 指向不被丢失 |
ts
复制代码
interface Query {
where<K extends keyof User>(
field: K,
val: User[K]
): ThisParameterType<typeof where>;
}
function where(this: Query[], field: any, val: any) {
return this.filter(u => u[field] === val);
}
const chain: Query & ThisParameterType<typeof where> = [] as any;
chain.where('name', 'Tom').where('age', 18); // 全程 this 被约束
12. 组合技:API 返回 → 隐藏敏感 → 局部更新 → 深度合并
把上面所有知识串一个完整案例:
ts
复制代码
// 1. 原始实体
interface User {
id: number;
name: string;
password: string;
profile: { avatar: { url: string }; bio: string };
}
// 2. 返回给前端的安全 DTO
type UserDto = Omit<User, 'password'>;
// 3. 更新时允许深度局部
type UserPatch = DeepPartial<UserDto>;
// 4. 实现:查 → 删敏感 → 合并 → 返
async function updateUser(id: number, patch: UserPatch): Promise<UserDto> {
const raw = await db.user.findUnique({ where: { id } });
if (!raw) throw new Error('404');
const sanitized: UserDto = exclude(raw, ['password']); // 手写工具
const merged = deepMerge(sanitized, patch);
await db.user.update({ where: { id }, data: merged });
return merged;
}
一键复制清单
技巧 |
一句话记忆 |
DeepPartial |
更新嵌套对象,不丢兄弟字段 |
Required + zod |
类型 & 运行时一次搞定 |
Readonly + immer |
状态不可变,写时复制 |
Pick 列 |
查多少返多少,带宽省 70% |
Omit 敏感 |
同一份实体,对外自动脱敏 |
Record 字典 |
枚举 key 全铺平,再配默认值函数 |
Exclude/Extract |
联合类型做"白名单/黑名单" |
NonNullable |
去掉 null,后面代码不再 ?. |
ReturnType + Parameters |
装饰器、包裹器必备,类型零丢失 |
ConstructorParameters + InstanceType |
工厂函数一行代码泛型化 |
ThisParameterType |
方法链、库作者专用,防 this 丢失 |
把上面 12 组代码全部跑通,你就在项目中把 TypeScript 内置工具类型"吃干抹净"了。
需要哪一条再单独抽出来封装为 npm utils,就能让团队永远告别"手写重复类型"的悲剧。祝你编码愉快!