Reputation: 123288
I am making an object that initially has only part of the type's required properties, so I am using Partial<Type>
to tell TypeScript that's OK.
Later I add the missing property (address
). Right now I'm using as
to tell TypeScript that the RawAccountWithAddress
is complete - i.e. that the type is now RawAccountWithAddress
rather than Partial<RawAccountWithAddress>
. Using as
feels like a hack though.
const rawAccountWithAddress: Partial<RawAccountWithAddress> = ...
rawAccountWithAddress.address = ...
return rawAccountWithAddress as RawAccountWithAddress;
Is this the best way to tell TypeScript the Partial<Type>
is now Type
? Shouldn't TypeScript just know that the partial is now complete, since the missing property has been assigned?
Is there a better way of doing this?
For reference the type is:
interface RawAccountWithAddress extends RawAccount {
address: PublicKey;
}
Upvotes: 0
Views: 1549
Reputation: 328748
You're expecting assignment narrowing to apply to the parent object when a property is set, but that doesn't happen, because it would involve synthesizing new object types anytime a property changes, and that would be expensive for the compiler. Still, there is an open issue suggesting this sort of gradual initialization at microsoft/TypeScript#35086, and a more general open issue for narrowing parent objects upon narrowing of their properties at microsoft/TypeScript#42384. If you're interested in seeing this happen you might want to go to those issues and give them a 👍 and/or comment describing why your use case is compelling and why workarounds aren't sufficient.
One workaround which may be sufficient, depending on the use case, is to wrap the concept of "setting a property narrows the parent" in its own assertion function. Here's one way to write it:
function setProp<
T extends object,
K extends PropertyKey,
V extends K extends keyof T ? T[K] : any
>(obj: T, key: K, val: V): asserts obj is
Extract<(T & Record<K, V>) extends infer O ? {
[P in keyof O]: O[P]
} : never, T> {
(obj as any)[key] = val;
}
That basically says that after you call setProp(obj, key, val)
, the compiler can narrow obj
from whatever type it started with to a version of that type where its known to have a key of key
whose value is the type of val
. Here's a sketch of how it works:
This is a generic function in three type parameters T
, K
, and V
corresponding to the three function parameters obj
, key
, and val
respectively.
The V
type parameter is constrained to a conditional type so that if obj
already has a key key
, then val
must be some subtype of the property value there. This will usually prevent someone from clobbering an existing property with an invalid type.
The output type is based on an intersection of the original type T
and the Record<K, V>
utility type meaning an object with properties of key type K
and value type V
. Then we take this intersection and use conditional type inference, mapped types and the Extract<T, U>
utility type to make the output type prettier than an intersection. If this is too confusing you could just use T & Record<K, V>
.
Again, it depends on use case whether this form works for you. The general idea is that we can convey that the act of calling obj[key] = val
can be treated as a narrowing on obj
.
Let's try it out:
interface Foo {
bar: string;
baz: number;
}
function makeFoo(): Foo {
const foo = {};
setProp(foo, "bar", "abc");
foo; // { bar: string; }
setProp(foo, "baz", 123);
foo; // { bar: string; baz: number }
return foo; // okay
}
That works whether or not you annotate foo
as Partial<Foo>
. You can see that foo
is narrowed from {}
to {bar: string}
and then to {bar: string; baz: number}
, after which it is seen as assignable to Foo
and accepted as the return value.
Upvotes: 4