Search code examples
typescriptmapped-types

Building mapped exclusive type in typescript


I have the following type (simplified):

type ValueRepresents = {
    boolean: true
    number?: false
    other?: false
} |
{
    boolean?: false
    number: true
    other?: false
} |
{
    boolean?: false
    number?: false
    other: true
}

My actual type has many more possible keys. Is there a way to generate this type from a list of possible keys to make it only valid to have one key with value set to true? Something like:

type ValueTypes = "boolean" | "number" | "other"
type ValueRepresents <T extends ValueTypes> = {
    [k in ValueTypes]: k extends T ? true : false
}
const a: ValueRepresents<"boolean"> = {
    boolean: true,
    number: false,
    other: false,
}

But I'm aiming for being able to use:

// should pass
const a: ValueRepresents = { boolean: true }

// should pass
const a2: ValueRepresents = {
    boolean: true,
    number: false,
}

// should error
const a3: ValueRepresents = {
    boolean: true,
    number: true,
}

// should error
const a4: ValueRepresents = {}

I also tried following this answer but was not yet successful with:

type ValueRepresents <T extends ValueTypes> = {
    [k in Exclude<T, ValueTypes>]?: false
} & { [k in T]: true }

Solution

  • You can try creating an union type like this

    type ValueTypes = "boolean" | "number" | "other"
    
    type ValueRepresents = ({
        [K in ValueTypes]: Partial<Record<Exclude<ValueTypes, K>, false>> & Record<K, true>
    })[ValueTypes]
    

    TypeScript Playground


    TL;DR

    I don't think this expression has a specific name. I've seen similar examples used in the docs in Advanced Types, but I'll try to explain how it works.

    type ValueRepresents = {
        [K in ValueTypes]: Partial<Record<Exclude<ValueTypes, K>, false>> & Record<K, true>
    }
    

    creates type equivalent to:

    type ValueRepresents = {
      boolean: {
        boolean: true;
        number?: false;
        other?: false;
      };
      number: {
        boolean?: false;
        number: true;
        other?: false;
      };
      other: {
        boolean?: false;
        number?: true;
        other: true;
      };
    };
    

    and by adding the union type in square brackets [ValueTypes] it extracts the values of those (all) keys in another union type equivalent to:

    type ValueRepresents =
      | {
          boolean: true;
          number?: false;
          other?: false;
        }
      | {
          boolean?: false;
          number: true;
          other?: false;
        }
      | {
          boolean?: false;
          number?: true;
          other: true;
        };