TypeScript:设为never的属性仍被判定为必填的问题
分阶段填充的TypeScript类型优化
问题背景
想实现一个分阶段逐步填充的Data类型,要求如下:
Data<DataType.bar>:仅需包含已完成的Foo对象,Bar和Baz无需存在Data<DataType.baz>:需包含Foo和Bar,Baz暂不需要Data<null>:包含Foo、Bar、Baz全部对象
最初的实现代码存在问题:当注释掉非必填属性时,TypeScript会报错提示缺失属性。后来写出了符合需求的实现,但代码重复度太高,希望找到更简洁的方案。
最初有问题的实现
enum DataType { 'foo', 'bar', 'baz', } type Foo = { a: string; }; type Bar = { b: string; }; type Baz = { c: string; }; type Data<T extends DataType | null = null> = { [DataType.foo]: T extends DataType.bar | DataType.baz | null ? Foo : never; [DataType.bar]: T extends DataType.baz | null ? Bar : never; [DataType.baz]: T extends null ? Baz : never; }; const myData: Data = { [DataType.foo]: { a: 'I should already exist while bar is being created' }, /* [DataType.bar]: { b: `I correctly error when uncommented... but when commented, DataType.bar also appears to be required?` } */ };
问题:注释掉DataType.bar后,TypeScript报错提示缺失[DataType.bar]和[DataType.baz]属性,不符合预期。
符合需求但重复的实现
type Data<T extends DataType | null = null> = T extends null ? { [DataType.foo]: Foo; [DataType.bar]: Bar; [DataType.baz]: Baz; } : T extends DataType.baz ? { [DataType.foo]: Foo; [DataType.bar]: Bar; } : T extends DataType.bar ? { [DataType.foo]: Foo; } : T extends DataType.foo ? {} : never;
优化方案
咱们可以用映射类型结合条件类型简化代码,避免重复定义每个阶段的结构,同时保证扩展性:
enum DataType { foo, bar, baz, } type Foo = { a: string }; type Bar = { b: string }; type Baz = { c: string }; // 定义枚举与对应类型的映射表 type TypeMap = { [DataType.foo]: Foo; [DataType.bar]: Bar; [DataType.baz]: Baz; }; // 根据传入的泛型T,筛选需要保留的前置属性 type PrecedingTypes<T extends DataType | null> = T extends null ? keyof TypeMap // 传null时保留所有属性 : T extends DataType.baz ? DataType.foo | DataType.bar // 传baz时保留foo和bar : T extends DataType.bar ? DataType.foo // 传bar时只保留foo : never; // 传foo时无属性 // 生成最终的Data类型 type Data<T extends DataType | null = null> = { [K in PrecedingTypes<T>]: TypeMap[K]; };
效果验证
const d1: Data<DataType.foo> = {};✅ 符合预期(空对象)const d2: Data<DataType.bar> = { [DataType.foo]: { a: "test" } };✅ 符合预期(仅包含foo)const d3: Data<DataType.baz> = { [DataType.foo]: { a: "test" }, [DataType.bar]: { b: "test" } };✅ 符合预期(包含foo和bar)const d4: Data<null> = { [DataType.foo]: { a: "test" }, [DataType.bar]: { b: "test" }, [DataType.baz]: { c: "test" } };✅ 符合预期(包含全部属性)
后续如果新增枚举值(比如qux),只需要在TypeMap中添加对应类型,并在PrecedingTypes中补充条件判断即可,无需重复编写整个对象结构,维护成本更低。
内容的提问来源于stack exchange,提问作者accidental_PhD
相关产品推荐
相关产品推荐

