如何为逐步添加属性的对象进行TypeScript类型定义?
逐步为非可选属性对象赋值的类型安全方案
我经常遇到这种场景:需要逐步给对象添加属性,属性值要经过计算才能确定。举个实际代码的例子:
declare const getString: () => string; declare const map: Map<string, unknown> type Thing = { foo: string; }; const thing: Thing = {} // 错误:类型“{}”中缺少类型“Thing”所需的属性“foo”。 const string = getString() // 假设此操作开销极大或仅能执行一次。 if (map.has(string)) thing.foo = string else thing.foo = "blank"
实际场景里属性会更多,核心问题是:这些属性必须是非可选的,但我没法在创建对象时就直接赋值;同时还得保留类型约束,防止给对象赋无效值。
我的尝试与遇到的问题
首次尝试:Partial类型转换
一开始我用Partial创建对象,再尝试转换成目标类型:
// 函数返回类型 function createThing(): Thing { const thing: Partial<Thing> = {} // ...赋值逻辑 return thing // 错误:Partial<Thing> 不能赋值给 Thing } // 变量赋值 const thing: Partial<Thing> = {} // ...赋值逻辑 const thing2: Thing = thing // 同样报错
这种方式行不通,因为Partial<Thing>的类型不会自动收窄,TypeScript始终认为它可能缺少属性。
尝试动态添加属性的函数
后来我试过写一个动态添加属性的函数,希望能更新对象类型:
const addProp = <T extends object, K extends keyof Thing, V>( obj: T, key: K, value: V ): T & Record<K, V> => Object.assign(obj, { [key]: value } as Record<K, V>); let thing: Partial<Thing> = {}; thing = addProp(thing, "foo", "blank");
函数返回的类型是对的,但变量thing的类型还是Partial<Thing>,每次赋值都得重新分配变量,很繁琐。
类型断言函数的局限
我也试过用类型断言函数,但这类函数要求必须指定所有泛型参数,使用起来不够灵活,没法自动推导。
可行的解决方案
方案1:手动断言+前置检查
如果属性数量不多,可以在确认所有属性都赋值完成后,用类型断言转换。关键是要手动保证所有必填属性都已设置:
declare const getString: () => string; declare const map: Map<string, unknown> type Thing = { foo: string; }; const thing: Partial<Thing> = {}; const string = getString(); if (map.has(string)) thing.foo = string; else thing.foo = "blank"; // 确认所有属性都已赋值后断言 const finalThing = thing as Thing;
方案2:自定义类型断言函数
写一个针对目标类型的断言函数,不用每次指定泛型,还能在运行时做校验:
type Thing = { foo: string; }; function assertIsThing(obj: unknown): asserts obj is Thing { if ( !obj || typeof obj !== 'object' || !('foo' in obj) || typeof (obj as Thing).foo !== 'string' ) { throw new Error('对象不符合Thing类型要求'); } } // 使用方式 const thing: Partial<Thing> = {}; const string = getString(); thing.foo = map.has(string) ? string : "blank"; assertIsThing(thing); // 此时thing的类型已被收窄为Thing console.log(thing.foo); // 类型为string,无报错
方案3:构建器模式(适合多属性场景)
如果对象有很多属性,可以用构建器类来逐步设置,最后返回完整类型的对象,同时在build方法里做必填属性校验:
type Thing = { foo: string; bar: number; }; class ThingBuilder { private thing: Partial<Thing> = {}; setFoo(value: string): this { this.thing.foo = value; return this; } setBar(value: number): this { this.thing.bar = value; return this; } build(): Thing { if (!this.thing.foo) throw new Error('foo是必填属性'); if (this.thing.bar === undefined) throw new Error('bar是必填属性'); return this.thing as Thing; } } // 使用方式 const string = getString(); const thing = new ThingBuilder() .setFoo(map.has(string) ? string : "blank") .setBar(123) .build(); // thing的类型是Thing,可直接使用
方案4:利用satisfies操作符(TypeScript 4.9+)
如果用的是TS 4.9及以上版本,可以用satisfies来验证中间状态的类型,最后再断言:
type Thing = { foo: string; }; const thing = {} satisfies Partial<Thing>; const string = getString(); thing.foo = map.has(string) ? string : "blank"; // 验证并转换类型 const finalThing = thing satisfies Thing as Thing;
内容的提问来源于stack exchange,提问作者Darryl Noakes
相关产品推荐
相关产品推荐

