Conditional types declaration

I'm not sure if I can do this, but I was wondering if based on the value of a key of my interface (which type is an enum), I can conditionally select what type another key of the same interface is going to be. I don't think you are going to understand without an example so, here we go.

I have some types declaration that looks basically like this:

interface BookingTextInput {
  type: InputType.text;
  metadata: TextInputMetadata;
}

interface BookingSelectInput {
  type: InputType.selectGrid;
  metadata: SelectInputMetadata;
}

interface BookingNumberInput {
  type: InputType.number;
  metadata: NumberInputMetadata;
}

type BookingInput = BookingTextInput | BookingNumberInput | BookingSelectInput;

And what I want to do is to avoid having to do an interface for each InputType enum value and instead do a BookingInput interface that conditionally see what value is in the type key and based on that, it knows what type is the metadata key.

Upvotes: 1

Views: 63

Answers (2)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249486

You can use a interface to map from enum type to the metadata interface and then use a mapped type to get a union that is functionally equivalent to what you have now:

interface InputTypeToInterfaceMap {
  [InputType.text]: TextInputMetadata;
  [InputType.selectGrid]: TextInputMetadata;
  [InputType.number]: TextInputMetadata;
}

type BookingInput = { 
    [P in keyof InputTypeToInterfaceMap]: {
        type: P; metadata: InputTypeToInterfaceMap[P] 
    } 
}[keyof InputTypeToInterfaceMap];

This makes the code a lot more terse and you only have to modify the enum and the mapping interface to add a new type.

A shorter way would be to not use an enum at all. I find the need for enums greatly reduced since string literal types were added. Using the interface map with simple enums and using keyof to extract the union of keys can achieve the same type safety goals with less changes when you want to add a new type:

interface InputTypeToInterfaceMap {
    text: TextInputMetadata;
    selectGrid: TextInputMetadata;
    number: TextInputMetadata;
}

type InputType = keyof InputTypeToInterfaceMap //  "number" | "text" | "selectGrid"
type BookingInput = {
    [P in keyof InputTypeToInterfaceMap]: {
        type: P; metadata: InputTypeToInterfaceMap[P]
    }
}[keyof InputTypeToInterfaceMap];

Upvotes: 1

shadeglare
shadeglare

Reputation: 7536

You can use Type Guards. Here's a little example how it works:

type Primitive = string | number | boolean;

function isString(x: Primitive): x is string {
    return typeof x === "string";
}

function isNumber(x: Primitive): x is number {
    return typeof x === "number";
}

function isBoolean(x: Primitive): x is boolean {
    return typeof x === "boolean";
}

let str: Primitive = "text";
let nmb: Primitive = 1;
let bln: Primitive = true;

let prms = [str, nmb, bln];

for (let x of prms) {
    if (isString(x)) {
        console.log(`string value: ${x}`);
    } else if (isNumber(x)) {
        console.log(`number value: ${x}`);
    } else if (isBoolean) {
        console.log(`boolean value: ${x}`);
    } else {
        console.log(`unknown type value: ${x}`);
    }
}

But for complex types I'd recommend to use Union Types as in your example.

Upvotes: 1

Related Questions