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
update(value: MyType, new_foo_val?: string) { return merge(value, { foo: new_foo_val })
. – Hockeynew_foo_val
isundefined
, thenmerge
will overridevalue.foo
withundefined
, which will result in{...value, foo: undefined}
, and that's not compatible withMyType
, which specifies thatfoo
is always astring
. – Microbiology