fcrick
fcrick

Reputation: 504

Can I make specifying one property require another be set, too?

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

Answers (3)

J. Pichardo
J. Pichardo

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

artem
artem

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

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

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

Related Questions