Home > front end >  Typescript Create a type whose properties are the properties of an interface
Typescript Create a type whose properties are the properties of an interface

Time:02-01

I have an interface definition as follows

export interface RoutingMap {
    ticket: {
        id: number
    },
    car: {
        model: string,
        make: number
    }
}

I want to be able to create an interface for an object, which has 2 properties - 1 being a key in the RoutingMap and the other being whatever the type of that Key in the RoutingMap is.

In pseudo-code I image it looking like this

export interface RoutingMessage {
    resource: keyof RoutingMap,
    params: RoutingMap["resource"]
}

The end goal is for me to be able to construct objects like this

const msg: RoutingMessage = {
    resource: "ticket",
    params: { id: 10 }
}

const invalidMsg: RoutingMessage = {
    resource: "something", //TS error - "something" is not a key of routing map
    params: { id: 10 }
}

const invalidMsg2: RoutingMessage = {
    resource: "car"
    params: { model: "tesla", make: true } //TS error - "make" is not a boolean
}

const invalidMsg3: RoutingMessage = {
    resource: "car"
    params: { id: 123 } //TS error - id is not assinable to  {model: string, make: boolean}
}

The RoutingMap is something which would be extended with time, and people should be able to create specific objects, based on the keys (and their values)

As you can see from the pseudo-code example I can set a constraint for the resource, but I need a way to constrain the params property to only allow objects, which match the signature of the Key in the RoutingMap

CodePudding user response:

Because there are a finite number of keys that match keyof RoutingMap, you can write RoutingMessage as a union of object types where each element of the union corresponds to a particular key, like this:

type RoutingMessage = {
    resource: "ticket";
    params: {
        id: number;
    };
} | {
    resource: "car";
    params: {
        model: string;
        make: number;
    };
}

You can compute RoutingMessage programmatically from RoutingMap by writing it as a "distributive object type" (terminology borrowed from microsoft/TypeScript#47109) where we make a mapped type where each property with key K in RoutingMap is mapped to the desired object type for that key, and then we immediately index into it with keyof RoutingMap, producing the desired union.

type RoutingMessage = { [K in keyof RoutingMap]:
  { resource: K, params: RoutingMap[K] }
}[keyof RoutingMap];

With the above definition, RoutingMessage will gain new union members whenever a new property is added to RoutingMap, as desired.


Let's make sure it behaves how you want:

const msg: RoutingMessage = {
  resource: "ticket",
  params: { id: 10 }
}; // okay

const invalidMsg: RoutingMessage = {
  resource: "something", // error!
  //~~~~~~ <-- Type '"something"' is not assignable to type '"ticket" | "car"'
  params: { id: 10 }
};

const invalidMsg2: RoutingMessage = {
  resource: "car",
  params: { model: "tesla", make: true } // error!
  //  --------------------> ~~~~
  // Type 'boolean' is not assignable to type 'number'.
};

const invalidMsg3: RoutingMessage = {
  resource: "car",
  params: { id: 123 } // error!
  // -----> ~~~~~~~
  // Type '{ id: number; }' is not assignable to type '{ model: string; make: number; }' 
};

Looks good!

Playground link to code

CodePudding user response:

export interface RoutingMessage {
    resource: keyof RoutingMap,
    params: RoutingMap[this["resource"]]
}
  •  Tags:  
  • Related