Reputation: 504
Here's an example. Assume I don't control the data format.
type Thing = {
name: string
}
// if you canJump, you need to specify howHigh
type Jump = {
canJump: true
howHigh: number
}
// MightJump Things either jump or don't
type MightJump = Thing | (Thing & Jump)
const thing: MightJump = {
name: 'hi',
canJump: true,
// howHigh: 5, // how do I make commenting this out an error?
}
So, ideally, for values with type MightJump
, if only name
is set, that's fine, but if either canJump
or howHigh
is set, the other in that pair is also required.
Is this possible in TypeScript? If not, what manner of change to the compiler would be required to support it? I'm very interested in the exact nature of the limitation.
From what I can tell, Thing | (Thing & Jump)
results in a type where both canJump
and howHigh
are just optional, and only name
remains required.
Update: Using artem's answer below:
type OptNever<T> = {
[K in keyof T]?: never
}
type AllOrNone<T> = T | OptNever<T>
type MightJump = Thing & AllOrNone<Jump>
Upvotes: 3
Views: 74
Reputation: 3115
One way to do it would be to have a negating type, something like:
type CantJump = {
canJump?: undefined
howHigh?: undefined
}
And then your MightJump
type would be
type MightJump = Thing & ( Jump | CantJump )
That way you are enforcing both properties or none.
There is currently a proposal for an unlike
keyword which would possibly allow something like:
type MightJump = Thing & ( Jump | unlike Jump )
However, is waiting for more feedback, meaning that there hasn't been enough support for it.
Upvotes: 1
Reputation: 51619
You can disallow the presence of a property in a member of a union type by giving it an optional never
type:
type Thing = {
name: string
}
// if you canJump, you need to specify howHigh, and vice versa
type JumpOrNot = { canJump?: never; howHigh?: never} | {
canJump: true
howHigh: number
}
// MightJump Things either jump or don't
type MightJump = Thing & JumpOrNot
const thing: MightJump = {
name: 'hi',
canJump: true,
// error
// Type '{ name: string; canJump: true; }' is not assignable to type 'MightJump'.
// Type '{ name: string; canJump: true; }' is not assignable to type 'Thing & { canJump: true; howHigh: number; }'.
// Type '{ name: string; canJump: true; }' is not assignable to type '{ canJump: true; howHigh: number; }'.
// Property 'howHigh' is missing in type '{ name: string; canJump: true; }'.
}
const thing1: MightJump = {
name: 'thing1'
}
const thing2: MightJump = {
name: 'thing2',
canJump: true,
howHigh: 5,
}
Upvotes: 1
Reputation: 249606
You need to ensure the types of the union are incompatible
type Thing = {
name: string
}
// if you canJump, you need to specify howHigh
type Jump = {
canJump: true
howHigh: number
}
// MightJump Things either jump or don't
type MightJump = (Thing & { canJump?: false } )| (Thing & Jump)
const thing: MightJump = {
name: 'hi',
canJump: true,
// howHigh: 5, // error
}
This is a bit of quirky behavior in how checking for extra proepries works for unions. Normally if you assign an object literal with extra properties the compiler would complain, but if the properties belong to any type of the union it will not. So your literal is assignable toThing
just with some extra properties. If we make it explicitly incompatible withThing
we get an error.
Upvotes: 1