Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I create a mapped type from the keys of a fixed object in Typescript

I have an object like this:

const routes = {
  home: { path: '/', page: 'home' },
  profile: { path: '/profile', page: 'users/profile' }
}

I would like to define a derived type from this, like so:

type RouteName = keyof typeof routes, which creates a type like "home" | "profile".

However, I can't then do:

for (let name in routes) {
  router.add({ name, ...routes[name]})
}

because the compiler complains about routes[name] being of implicit type any:

Element implicitly has an 'any' type because type '{ home: { path: string; page: string; }; profile: { path: string; page: string; };' has no index signature.

If I modify the definition of routes to:

interface RouteDefinition {
  path: string
  page: string
}
const routes: {[key: string]: RouteDefinition} = {
  home: { path: '/', page: 'home' },
  profile: { path: '/profile', page: 'users/profile' }
}

the generated type type RouteName = keyof typeof routes is now string instead of "home"|"profile".

I could of course define a hardcoded RouteName type, but in case it isn't clear, I'm trying to avoid having to define the route names in two places, especially when the keys of the object strictly define the set of possibilities.

The object only needs to be defined once, and never needs to be reassigned. I've tried a bunch of combinations of Readonly<>, casting etc, but can't figure it out. Is there a way to do this?

(I'm using Typescript 2.8.1)

like image 782
majelbstoat Avatar asked Apr 27 '18 21:04

majelbstoat


People also ask

How would you define type for object keys in TypeScript?

Use the keyof typeof syntax to create a type from an object's keys, e.g. type Keys = keyof typeof person . The keyof typeof syntax returns a type that represents all of the object's keys as strings.

How do you define a Map type in TypeScript?

Use Map type and new keyword to create a map in TypeScript. let myMap = new Map<string, number>(); To create a Map with initial key-value pairs, pass the key-value pairs as an array to the Map constructor.


1 Answers

TypeScript doesn't consider it safe to assume that for..in keys are exactly the keys defined in the type, because in JavaScript all objects are open.

You can use an assertion to make the compile error go away:

for (let name in routes) {
  routes[name as RouteName]; // no error
}

Or, what I would do is combine your two approaches. You can define your routes as you are doing, extract the keys as a RouteName, but also make a RouteDefinition and assign your routes to an indexed type (which could be done using an assertion to a new variable, or a function parameter) when you want to map over them:

interface RouteDefinition {
    path: string;
    page: string;
}

const routes = {
    home: { path: '/', page: 'home' },
    profile: { path: '/profile', page: 'users/profile' }
}

type RouteName = keyof typeof routes;

mapRoutes(routes);

function mapRoutes(routes: { [key: string]: RouteDefinition }) {
    for (let name in routes) {
        routes[name] // no error
    }
}

If your routes literal doesn't satisfy the RouteDefinition (missing a key, key of wrong type) then you'll get an error at the assignment site, ie mapRoutes(routes) above.

like image 150
Aaron Beall Avatar answered Nov 15 '22 04:11

Aaron Beall