To be honest I have no clue how to name this issue properly.
TypeScript Version: 3.4.0-dev.201xxxxx
Search Terms:
Code
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
type ContextTitle =
{ title?: string } |
{ title: string, shortcut?: string }
type UselessStuff = {
name: string,
age: number
}
type DeleteStuff = Omit<UselessStuff, 'name'>
type T = ContextTitle & DeleteStuff
const a: T = {
age: 10,
shortcut: 'hey'
}
Expected behavior:
Get an error because I've defined shortcut, but not the required property title
Actual behavior:
Typescript does not report any error. Apparently it correctly detects the problem when using UselessStuff instead of the "Omitted" version.
Playground Link: http://www.typescriptlang.org/play/index.html#src=type%20Omit%3CT%2C%20K%20extends%20keyof%20T%3E%20%3D%20Pick%3CT%2C%20Exclude%3Ckeyof%20T%2C%20K%3E%3E%0A%0Atype%20ContextTitle%20%3D%0A%20%20%7B%20title%3F%3A%20string%20%7D%20%7C%0A%20%20%7B%20title%3A%20string%2C%20shortcut%3F%3A%20string%20%7D%0A%0Atype%20UselessStuff%20%3D%20%7B%0A%20%20name%3A%20string%2C%0A%20%20age%3A%20number%0A%7D%0A%0Atype%20DeleteStuff%20%3D%20Omit%3CUselessStuff%2C%20%27name%27%3E%0A%0A%0Atype%20T%20%3D%20ContextTitle%20%26%20DeleteStuff%0A%0Aconst%20a%3A%20T%20%3D%20%7B%0A%20%20age%3A%2010%2C%0A%20%20shortcut%3A%20%27hey%27%0A%7D%20%0A
A smaller repro:
type ContextTitle =
{ title?: string, age: number } |
{ title: string, age: number, shortcut?: string }
const a: ContextTitle = {
age: 10,
shortcut: 'hey'
}
I think this is not a bug just a side effect of how unions and excess property checks (EPC) work. EPC for unions will not raise an error if the key is present on any member of the union, so both age and shortcut are ok in the object literal as far as EPC is concerned.
Secondly, when the object literal is checked for compatibility with the union, the assignment is valid if the object literal is valid for any member of the union. But you might say "That object literal is not a valid value for any member of the union". Not true, excluding EPC, { age: 10, shortcut: 'hey'} is compatible with { title?: string, age: number }. It does not have title but that is optional anyway, it has age as required, and has an extra property that, as we stated earlier EPC will not complain about.
The solution is to add a member make the union members incompatible:
type ContextTitle =
{ title?: string, age: number, shortcut?: undefined } |
{ title: string, age: number, shortcut?: string }
const a: ContextTitle = { age: 10, shortcut: 'hey'} //err
Or a more general approach, using StrictUnion (also found here although that exact version might have some issued in 3.4):
type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAllKeys extends PropertyKey> = T extends any ? T & Partial<Record<Exclude<TAllKeys, keyof T>, undefined>> : never;
type StrictUnion<T> = StrictUnionHelper<T, UnionKeys<T>>
type ContextTitle = StrictUnion<
{ title?: string, age: number } |
{ title: string, age: number, shortcut?: string }>
const a: ContextTitle = { age: 10, shortcut: 'hey'} //err
Thanks — that kind of explains the issues; I figured out with some external help that another way to make this happen is
type ContextTitle =
{ title?: string, shortcut: never } |
{ title: string, shortcut?: string }
my few cents, a better practice is to use a dedicated discriminator property
using booleans
type ContextTitle =
| { isThat: true, title?: string}
| { isThat: false, title: string, shortcut?: string }
or anything else
type ContextTitle =
| { kind: 'this', title?: string }
| { kind: 'that', title: string, shortcut?: string }
I tend to agree, but unfortunately this would be kind of a breaking change that I can't really do right now.
Most helpful comment
A smaller repro:
I think this is not a bug just a side effect of how unions and excess property checks (EPC) work. EPC for unions will not raise an error if the key is present on any member of the union, so both
ageandshortcutare ok in the object literal as far as EPC is concerned.Secondly, when the object literal is checked for compatibility with the union, the assignment is valid if the object literal is valid for any member of the union. But you might say "That object literal is not a valid value for any member of the union". Not true, excluding EPC,
{ age: 10, shortcut: 'hey'}is compatible with{ title?: string, age: number }. It does not havetitlebut that is optional anyway, it hasageas required, and has an extra property that, as we stated earlier EPC will not complain about.The solution is to add a member make the union members incompatible:
Or a more general approach, using
StrictUnion(also found here although that exact version might have some issued in 3.4):