实现 DeepReadonly 等深度类型
问题
如何实现 DeepReadonly、DeepPartial、DeepRequired 等深度递归的工具类型?
答案
深度工具类型通过递归应用映射类型实现,关键是正确判断何时继续递归、何时停止,以及处理函数、数组等特殊类型。
DeepReadonly 实现
基础版本
// 内置 Readonly 是浅层的
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// DeepReadonly 递归应用
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
处理函数类型
// 函数不需要递归处理
type DeepReadonly<T> = T extends (...args: any[]) => any
? T // 函数保持原样
: {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
完善版本
type DeepReadonly<T> = T extends (...args: any[]) => any
? T // 函数
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T; // 原始类型
// 使用示例
interface User {
id: number;
profile: {
name: string;
address: {
city: string;
zip: string;
};
};
tags: string[];
greet(): void;
}
type ReadonlyUser = DeepReadonly<User>;
// {
// readonly id: number;
// readonly profile: {
// readonly name: string;
// readonly address: {
// readonly city: string;
// readonly zip: string;
// };
// };
// readonly tags: readonly string[];
// readonly greet: () => void;
// }
const user: ReadonlyUser = {
id: 1,
profile: { name: 'Alice', address: { city: 'Beijing', zip: '100000' } },
tags: ['admin'],
greet() {}
};
// user.id = 2; // 错误
// user.profile.name = 'Bob'; // 错误
// user.profile.address.city = 'Shanghai'; // 错误
// user.tags.push('user'); // 错误
DeepPartial 实现
所有属性及嵌套属性都变为可选:
type DeepPartial<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// 使用示例
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
database: {
url: string;
pool: number;
};
}
type PartialConfig = DeepPartial<Config>;
// 可以只提供部分配置
const config: PartialConfig = {
server: {
host: 'localhost'
// port 和 ssl 都是可选的
}
// database 也是可选的
};
应用:配置合并函数
function deepMerge<T extends object>(
target: T,
source: DeepPartial<T>
): T {
const result = { ...target };
for (const key in source) {
const sourceValue = source[key];
const targetValue = target[key];
if (
typeof sourceValue === 'object' &&
sourceValue !== null &&
typeof targetValue === 'object' &&
targetValue !== null
) {
(result as any)[key] = deepMerge(
targetValue as object,
sourceValue as DeepPartial<typeof targetValue>
);
} else if (sourceValue !== undefined) {
(result as any)[key] = sourceValue;
}
}
return result;
}
const defaultConfig: Config = {
server: { host: '0.0.0.0', port: 3000, ssl: { enabled: false, cert: '' } },
database: { url: 'localhost', pool: 10 }
};
const merged = deepMerge(defaultConfig, {
server: { port: 8080, ssl: { enabled: true } }
});
// server.host 保持 '0.0.0.0'
// server.port 变为 8080
// ssl.enabled 变为 true,ssl.cert 保持 ''
DeepRequired 实现
所有属性及嵌套属性都变为必选:
type DeepRequired<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;
// 使用示例
interface User {
name?: string;
profile?: {
bio?: string;
avatar?: {
url?: string;
alt?: string;
};
};
}
type RequiredUser = DeepRequired<User>;
// {
// name: string;
// profile: {
// bio: string;
// avatar: {
// url: string;
// alt: string;
// };
// };
// }
DeepMutable 实现
移除所有 readonly 修饰符:
type DeepMutable<T> = T extends (...args: any[]) => any
? T
: T extends object
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
: T;
// 使用示例
interface ReadonlyState {
readonly user: {
readonly id: number;
readonly name: string;
};
readonly items: readonly string[];
}
type MutableState = DeepMutable<ReadonlyState>;
// {
// user: {
// id: number;
// name: string;
// };
// items: string[];
// }
处理数组类型
数组需要特殊处理以保持数组结构:
// 更完善的 DeepReadonly
type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends Map<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends Set<infer U>
? ReadonlySet<DeepReadonly<U>>
: T extends Array<infer U>
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// 测试
type Test = DeepReadonly<{
arr: number[];
map: Map<string, { value: number }>;
set: Set<string>;
}>;
// {
// readonly arr: readonly number[];
// readonly map: ReadonlyMap<string, { readonly value: number }>;
// readonly set: ReadonlySet<string>;
// }
DeepNonNullable 实现
移除所有 null 和 undefined:
type DeepNonNullable<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]: DeepNonNullable<NonNullable<T[K]>> }
: NonNullable<T>;
// 使用示例
interface MaybeUser {
id: number | null;
name: string | undefined;
profile: {
bio: string | null;
avatar: string | undefined | null;
} | null;
}
type DefiniteUser = DeepNonNullable<MaybeUser>;
// {
// id: number;
// name: string;
// profile: {
// bio: string;
// avatar: string;
// };
// }
DeepNullable 实现
所有属性都可以为 null:
type DeepNullable<T> = T extends (...args: any[]) => any
? T | null
: T extends object
? { [K in keyof T]: DeepNullable<T[K]> } | null
: T | null;
// 使用示例
interface User {
id: number;
profile: {
name: string;
age: number;
};
}
type NullableUser = DeepNullable<User>;
// 可以赋值为:
const user1: NullableUser = null;
const user2: NullableUser = { id: null, profile: null };
const user3: NullableUser = { id: 1, profile: { name: null, age: null } };
DeepRecord 实现
深度将所有值类型替换为指定类型:
type DeepRecord<T, V> = T extends (...args: any[]) => any
? T // 保持函数不变
: T extends object
? { [K in keyof T]: DeepRecord<T[K], V> }
: V;
// 使用示例:创建表单错误类型
interface FormValues {
username: string;
password: string;
profile: {
name: string;
bio: string;
};
}
type FormErrors = DeepRecord<FormValues, string | undefined>;
// {
// username: string | undefined;
// password: string | undefined;
// profile: {
// name: string | undefined;
// bio: string | undefined;
// };
// }
// 创建表单 touched 状态
type FormTouched = DeepRecord<FormValues, boolean>;
高级:递归深度限制
防止无限递归:
type DeepReadonlyWithDepth<T, Depth extends number = 5> = Depth extends 0
? T
: T extends (...args: any[]) => any
? T
: T extends object
? {
readonly [K in keyof T]: DeepReadonlyWithDepth<
T[K],
MinusOne<Depth>
>;
}
: T;
// 辅助:减一
type MinusOne<N extends number> = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9][N];
// 深度为 2
type ShallowDeep = DeepReadonlyWithDepth<{
a: { b: { c: { d: number } } };
}, 2>;
// a 和 a.b 是 readonly,a.b.c 不是
常见面试问题
Q1: 为什么要特殊处理函数类型?
答案:
// 不处理函数的问题
type BrokenDeepReadonly<T> = {
readonly [K in keyof T]: BrokenDeepReadonly<T[K]>;
};
// 当 T 是函数时
type Fn = () => void;
type Test = BrokenDeepReadonly<Fn>;
// {} 或错误结果,因为函数的 keyof 是其静态属性
// 函数应该保持原样
type DeepReadonly<T> = T extends (...args: any[]) => any
? T // 函数直接返回
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type Test2 = DeepReadonly<Fn>;
// () => void,保持不变
Q2: 如何正确处理数组?
答案:
// 简单递归会把数组变成对象
type BadDeep<T> = T extends object
? { [K in keyof T]: BadDeep<T[K]> }
: T;
// 数组有 length、push 等属性被遍历
// 正确处理
type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends Array<infer U>
? ReadonlyArray<DeepReadonly<U>> // 数组特殊处理
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// 测试
type StringArray = DeepReadonly<string[]>;
// readonly string[]
type NestedArray = DeepReadonly<{ items: { name: string }[] }>;
// { readonly items: readonly { readonly name: string }[] }
Q3: 实现 DeepPartial 并确保可以赋值空对象
答案:
// 基础版本需要提供所有嵌套结构
type BasicDeepPartial<T> = T extends object
? { [K in keyof T]?: BasicDeepPartial<T[K]> }
: T;
// 问题:{} 不能赋值给需要嵌套结构的类型
// 因为可选属性仍然要求正确的类型
// 解决方案:允许 undefined
type DeepPartial<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> | undefined }
: T;
// 现在可以
const config: DeepPartial<{
server: {
host: string;
port: number;
};
}> = {}; // OK
const config2: DeepPartial<{
server: {
host: string;
port: number;
};
}> = {
server: {} // OK,因为属性是 undefined | { host?: ...; port?: ... }
};
Q4: 如何实现 DeepPick?
答案:
// 路径式深度选择
type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? PathValue<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;
// 构建深度对象
type DeepPick<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? { [Key in K]: DeepPick<T[K], Rest> }
: never
: P extends keyof T
? { [Key in P]: T[P] }
: never;
// 使用
interface User {
id: number;
profile: {
name: string;
address: {
city: string;
zip: string;
};
};
}
type City = DeepPick<User, 'profile.address.city'>;
// { profile: { address: { city: string } } }
// 多路径选择(联合)
type MultiPick = DeepPick<User, 'id'> & DeepPick<User, 'profile.name'>;
// { id: number } & { profile: { name: string } }
Q5: DeepReadonly 和 as const 的区别?
答案:
// as const:将值的类型变为字面量,且所有属性变为 readonly
const config = {
api: 'https://api.example.com',
timeout: 5000,
features: ['a', 'b']
} as const;
// typeof config = {
// readonly api: "https://api.example.com";
// readonly timeout: 5000;
// readonly features: readonly ["a", "b"];
// }
// DeepReadonly:类型层面的转换,不改变字面量类型
interface Config {
api: string;
timeout: number;
features: string[];
}
type ReadonlyConfig = DeepReadonly<Config>;
// {
// readonly api: string; // 不是字面量
// readonly timeout: number; // 不是字面量
// readonly features: readonly string[];
// }
// 区别:
// 1. as const 用于值,推断字面量类型
// 2. DeepReadonly 用于类型转换,保持类型不变
// 3. as const 只能用于字面量对象
// 4. DeepReadonly 可以用于任何类型
// 组合使用
const data = {
name: 'Alice',
settings: { theme: 'dark' }
} as const satisfies DeepReadonly<{
name: string;
settings: { theme: string };
}>;