Warning! This is my interpretation (theory) of what happens. It isn't confirmed.
The issue seems to be an intended behavior of handling optional fields.
When you define this type:
interface MyTypeOptional {
value?: number;
}
TypeScript expects that the value would be of type number | never.
So when you spread it
const withOptional: MyTypeOptional = {};
const spread = { ...withOptional };
The type of spread object should be either { value: never | number } or { value?: number }.
Unfortunately, due to the ambiguous usage of optional field vs undefined TypeScript infers the spread object as such:
value?: number | undefined
So the optional field type is cast into number | undefined when spreading.
What doesn't make sense is that when we spread a non-optional type it seems to override the optional type:
interface MyTypeNonOptional {
value: number;
}
const nonOptional = { value: 1 };
const spread2 = { ...nonOptional, ...withOptional }
Looks like a bug? But no, here TypeScript doesn't cast optional field into number | undefined. Here it casts it to number | never.
We can confirm this by changing optional into explicit |undefined:
interface MyTypeOptional {
value: number | undefined;
}
Here, the following spread:
const withOptional: MyTypeOptional = {} as MyTypeOptional;
const nonOptional = { value: 1 };
const spread2 = { ...nonOptional, ...withOptional }
would be inferred as { value: number | undefined; }
And when we change it to optional or undefined:
interface MyTypeOptional {
value?: number | undefined;
}
it is broken again and inferred as { value: number }
So what's a workaround?
I would say, don't use optional fields until TypeScript team fixes them.
This has several implications:
- If you need the key of the object to be omitted if undefined, use
omitUndefined(object)
- If you need to use Partial, limit its usage by making a factory:
<T>createPartial (o: Partial<T>) => Undefinable<T>. So that the result of createPartial<MyTypeNonOptional>({}) would be inferred as { value: undefined } or Undefinable<MyTypeNonOptional>.
- If you need a value of a field to be
unset, use null for that.
There's an open issue about this limitation of optional fields:
https://github.com/microsoft/TypeScript/issues/13195