How to create a kinda-Partial<T>
type, that doesn't allow undefined
values?
Here's an example:
interface MyType { foo: string bar?: number } const merge = (value1: MyType, value2: KindaPartial<MyType>): MyType => { return {...value1, ...value2}; } const value = { foo: 'foo', bar: 42 } merge(value, {}); // should work merge(value, { foo: 'bar' }); // should work merge(value, { bar: undefined }); // should work merge(value, { bar: 666 }); // should work merge(value, { foo: '', bar: undefined }); // should work merge(value, { foo: '', bar: 666 }); // should work // now the problematic case: merge(value, { foo: undefined }); // this should throw an error // because MyType["foo"] is of type string
The type I'm looking for should:
Partial<T>
)undefined
if the generic type doesn't accept undefined
for that keyIs this possible?
EDIT: I also created a issue at TypeScript repository, because this is weird and should throw an error at some point: https://github.com/Microsoft/TypeScript/issues/29701
TS 4.4 UPDATE:
TS4.4 will have an --exactOptionalPropertyTypes
compiler flag to give you the behavior youβre looking for directly with Partial
, as long as you intentionally add undefined
where you'd like to allow it:
interface MyType { foo: string bar?: number | undefined // <-- you want this } const merge = (value1: MyType, value2: Partial<MyType>): MyType => { return { ...value1, ...value2 }; } const value = { foo: 'foo', bar: 42 } merge(value, {}); // okay merge(value, { foo: 'bar' }); // okay merge(value, { bar: undefined }); // okay merge(value, { bar: 666 }); // okay merge(value, { foo: '', bar: undefined }); // okay merge(value, { foo: '', bar: 666 }); // okay // now the problematic case: merge(value, { foo: undefined }); // error! // ----------> ~~~ // Type 'undefined' is not assignable to type 'string'
Playground link to code <-- note, currently you will need to turn on --exactOptionalPropertyTypes
yourself in the TS Config tab; for some reason the url is broken
ββ--
PRE-TS4.4 ANSWER:
It is a known limitation (see microsoft/TypeScript#13195) that TypeScript doesn't properly distinguish between object properties (and function parameters) which are missing from ones with are present but undefined
. The fact that Partial<T>
allows undefined
properties is a consequence of that. The right thing to do is to wait until this issue is addressed (this might become more likely if you go to that issue in GitHub and give it a π or a comment with a compelling use case).
If you don't want to wait, you can maybe use the following hacky way to get something like this behavior:
type VerifyKindaPartial<T, KP> = Partial<T> & {[K in keyof KP]-?: K extends keyof T ? T[K] : never}; const merge = <KP>(value1: MyType, value2: KP & VerifyKindaPartial<MyType, KP>): MyType => { return { ...value1, ...value2 }; }
So you can't write KindaPartial<T>
directly. But you can write a type VerifyKindaPartial<T, KP>
that takes a type T
and a candidate type KP
that you want to check against your intended KindaPartial<T>
. If the candidate matches, then it returns something that matches KP
. Otherwise it returns something that does not.
Then, you make merge()
a generic function that infers KP
from the type of the value passed into value2
. If KP & VerifyKindaPartial<MyType, KP>
matches KP
(meaning that KP
matches KindaPartial<MyType>
), then the code will compile. Otherwise, if KP & VerifyKindaPartial<MyType, KP>
does not match KP
(meaning that KP
does not match KindaPartial<MyType>
), then there will be an error. (The error might not be very intuitive, though).
Let's see:
merge(value, {}); // works merge(value, { foo: 'bar' }); // works merge(value, { bar: undefined }); // works merge(value, { bar: 666 }); // works merge(value, { foo: '', bar: undefined }); // works merge(value, { foo: '', bar: 666 }); // works merge(value, { foo: undefined }); // error! // ~~~ <-- undefined is not assignable to never // the expected type comes from property 'foo',
That has the behavior you want... although the error you get is a bit weird (ideally it would say that undefined
is not assignable to string
, but the problem is that the compiler knows the passed-in type is undefined
, and it wants the type to be string
, so the compiler intersects these to undefined & string
which is never
. Oh well.
Anyway there are probably caveats here; generic functions work well when called directly but they don't compose well because TypeScript's support of higher-kinded types isn't that good. I don't know if this will actually work for your use case, but it's the best I can do with the language as it currently is.
Playground link to code
In this case, Pick
should work.
interface MyType { foo: string bar?: number } const merge = <K extends keyof MyType>(value1: MyType, value2: Pick<MyType, K>): MyType => { return {...value1, ...value2}; } merge(value, {}); // ok merge(value, { foo: 'bar' }); // ok merge(value, { bar: undefined }); // ok merge(value, { bar: 666 }); // ok merge(value, { foo: '', bar: undefined }); // ok merge(value, { foo: '', bar: 666 }); // ok merge(value, { foo: undefined }); // ng
Playground link
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With