4

How can I achieve this ?

type Fruit = "apple" | 'banana' | 'coconut'

type FruitCollection = { [f in Fruit]?: number }

const validFruitCollection: FruitCollection = { apple: 1, coconut: 2 } 

const emptyCollectionShouldNotPass: FruitCollection = {} // I don't want typescript to let this pass
Lev
  • 13,856
  • 14
  • 52
  • 84
  • Does this answer your question? [typescript interface require one of two properties to exist](https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist) – Murat Karagöz Mar 20 '20 at 11:21
  • This answer is exactly what you want https://stackoverflow.com/a/49725198/4467208 – Murat Karagöz Mar 20 '20 at 11:21
  • https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=12&pc=120# – Murat Karagöz Mar 20 '20 at 11:22
  • Murat the difference is here that in these examples types are presize, here we have very loose type. It means we cannot pick what exactly should stay – Maciej Sikora Mar 20 '20 at 11:33

2 Answers2

4

What we need is type which will exclude possibility of empty object. In order to achieve that we need utility type and value constructor. Consider:

type Fruit = "apple" | 'banana' | 'coconut'

type FruitCollection = { [f in Fruit]?: number }

// type which will exclude empty object
type NotEmpty<T> = {} extends T ? never : T

// value constructor
const makeFruitCollection = <T extends FruitCollection>(c: NotEmpty<T>) => c; 

// use cases
const validFruitCollection = makeFruitCollection({ apple: 1, coconut: 2 }) // ok 
const emptyCollectionShouldNotPass = makeFruitCollection({}) // error 

Type NotEmpty is checking if our T which already pass all needs of FruitCollection is not empty object, if it is we get never, and there is no value of type never therefor using function with {} will not compile.

The Playground

Maciej Sikora
  • 19,374
  • 4
  • 49
  • 50
  • 1
    If you are using a function, then this is a good option, the usual problem with this solution is that the type itself does not require at least one property, so you can end up with variables that do not respect the constraint. But for function parameters it is a good, arguably simpler to understand solution :) – Titian Cernicova-Dragomir Mar 20 '20 at 11:44
  • There is a built-in utility type: Exclude. Thus `NotEmpty` can be rewritten as `Exclude`? – weakish May 24 '20 at 08:54
4

You can intersect the type with all optional members with a union of all properties, where all in each constituent of the union, one member is required. So basically you will have:


type WhatWeWant = {
    apple?: number | undefined;
    banana?: number | undefined;
    coconut?: number | undefined;
} & (
    | { apple: number; }
    | { banana: number; }
    | { coconut : number ;})

To get this type without writing it out we can use a mapped type:


type RequireOne<T> = T & { [P in keyof T]: Required<Pick<T, P>> }[keyof T]
type FruitCollection = RequireOne<{ [f in Fruit]?: number }>

Playground Link

The idea of the mapped type in RequireOne is to create union in the WhatWeWant type above (T will be the original type will al the optional properties). So what we do, in the mapped type is we take each property in T and type it as Required<Pick<T, P>>. This means for each key, we get a type that only contains that key, basically this type for the example:

{
  apple: { apple: number; }
  banana: { banana: number; }
  coconut: { coconut: number ;}
}

With this type, the matter of getting the union we want is just a matter with indexing keyof T, to get a union of all property types in our object.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357