TypeScript Partial<T> type without undefined
It is a known limitation 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.
Hope that helps; good luck!
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