3

I'm trying to get this example to work like this:

interface Foo {
    a: number;
    b: string;
    c: boolean;
}

type Explode<T> = keyof T extends infer K
    ? K extends unknown
    ? { [I in keyof T]: I extends K ? T[I] : never }
    : never
    : never;


type Test = Explode<Foo>;

const test: Test = {a: 1};

which gives me the following error:

Type '{ a: number; }' is not assignable to type '{ a: number; b: never; c: never; } | { a: never; b: string; c: never; } | { a: never; b: never; c: boolean; }'.
  Type '{ a: number; }' is missing the following properties from type '{ a: number; b: never; c: never; }': b, c

How can I instantiate an object of type Test without the error? I want a type that can contain either field a or b or c (or be an empty {}).

Chris
  • 4,212
  • 5
  • 37
  • 52
  • Does this answer your question? [How to express "At least one of the exisiting properties AND no additional properties" with a mapped type](https://stackoverflow.com/questions/69246630/how-to-express-at-least-one-of-the-exisiting-properties-and-no-additional-prope) – captain-yossarian from Ukraine Sep 30 '21 at 07:36

2 Answers2

2

I found the solution. I needed to make Foos fields optional like this:

EDITED to include the suggestion by @apokryfos

interface Foo {
    a: number;
    b: string;
    c: boolean;
}

Now I can do:

interface Foo {
    a: number;
    b: string;
    c: boolean;
}

type AtMostOneOf<T> = keyof T extends infer K
    ? K extends unknown
    ? { [I in keyof T]+?: I extends K ? T[I] : never }
    : never
    : never;


type Test = AtMostOneOf<Foo>;

const test: Test = {a: 1}; // allowed
const test1: Test = {b: 'asd'}; // allowed
const test2: Test = {a: 1, b: 'asd'}; // not allowed
const test3: Test = {} // allowed

Playground

Chris
  • 4,212
  • 5
  • 37
  • 52
  • 1
    You don't need to make `Foo` properties optional within `Foo`. You can make them in the `Explode` type via `[I in keyof T]+?:` as in e.g. [this](https://www.staging-typescript.org/play?jsx=0&ts=4.3.5#code/JYOwLgpgTgZghgYwgAgGIHt3IN4ChkHJwBcyIArgLYBG0A3PodaQM5hSgDmDhyCp1TABsIcEAwC+uXGACeABxQBRAB7yh6ACYQAPABUAfMgC8yANYRZ6GMj3IIKyCE0tkoGNGQBpRgQD83vaOEM6u5CBmIOgA7iC+yAHYyADaAJJuIOaW1rYAugDUfqTpDk4ugQF6abnIpCAQAG6eUrx1jdDxbU1QDNJyirYQbCbIqupauhjoBr0I6CDDkGykekNgI9gkyACMEnTIAPQHREIa0RCauHML60tg2ytrG8zIAORwLJqve4fHcKcxC5XeaLNYAJkew1Mm1I2wANMgXu9Pt99kcyOh1v8zkDrqC2ABmSHraESX4nHGXIA) – apokryfos Sep 30 '21 at 08:29
0

You can define the Explode type as:


type Explode<T> = {
    [K in keyof T]: {
        [key in K]: T[K]
    }
}[keyof T]

then type Test will become:

type Test = {
    a: number;
} | {
    b: string;
} | {
    c: boolean;
}

TS Playground

Teneff
  • 30,564
  • 13
  • 72
  • 103
  • 1
    Thanks, but I need a type that allows only one (or none) of the 3 fields to be set. Your example allows more than one field to be set at the same time, e.g. `const test: Test = { a: 1, b: 'asd' };` – Chris Oct 04 '21 at 13:36