3

Say I have an array that looks like this:

const options =  [
 {
   name: 'foo',
   type: 'boolean'
 },

 {
   name: 'bar',
   type: 'string'
 },

 {
   name: 'bar', // should be baz not bar
   type: 'number'
 }

]

I am looking to use this array as an interface which would look something like this:

export interface Opts {
   foo: boolean,
   bar: string,
   baz: number
}

so that would probably have to be something like:

export type Opts = manipulate(typeof options);

where manipulate is some magical TS feature I hope to discover.

I believe this is a good place to start: https://blog.mariusschulz.com/2017/01/20/typescript-2-1-mapped-types

but it's hard to figure out.

1 Answer 1

6

Yes, you can do this, but it requires both mapped and conditional types.

First you need a type that represents your mapping from type names like "boolean", to actual types like boolean.

type TypeMapping = {
  boolean: boolean,
  string: string,
  number: number,
  // any other types
}

Then you need a helper function which makes sure your options value doesn't get its types for the name and type properties widened to string. (If you inspect your options value, its type is something like {name: string, type: string}[], which has lost track of the particular name and type values you want.) You can use generic constraints to do this, as follows:

const asOptions = <K extends keyof any, 
  T extends Array<{ name: K, type: keyof TypeMapping }>>(t: T) => t;

Let's see if it works:

const options = asOptions([
  {
    name: 'foo',
    type: 'boolean'
  },

  {
    name: 'bar',
    type: 'string'
  },

  {
    name: 'bar',
    type: 'number'
  }
]);

If you inspect that you will see that it is now an array of types where each of name and type are narrowed to the literals "foo", "bar", "number", etc.

Finally we have to do that manipulate type function you want. I'll call it OptionsToType:

type OptionsToType<T extends Array<{ name: keyof any, type: keyof TypeMapping }>>
  = { [K in T[number]['name']]: TypeMapping[Extract<T[number], { name: K }>['type']] }

That might seem very complicated. Let's see if I can break it down.

T extends Array<{ name: keyof any, type: keyof TypeMapping }>

means T must be an array of objects with a name field like an object key, and a type field like a key of the TypeMapping type above.

  = { [K in T[number]['name']]: ... }

iterate through all the key names in the name property from each element of the T array

  Extract<T[number], { name: K }>

means "find the element of T that corresponds to the name K"...

  Extract<T[number], { name: K }>['type']

...and look up its 'type' property...

  TypeMapping[Extract<T[number], { name: K }>['type']]

...and use that as an index into the TypeMapping type.

Okay let's see if it works:

export type Opts = OptionsToType<typeof options>;

And if you inspect Opts you see:

{
    foo: boolean;
    bar: string | number;
}

just as you expected--- uh, wait, why is the bar property of type string | number? Oh, because you put bar in options twice. Change the second one to baz and it will be what you expect.

Okay, hope that helps. Good luck!

Sign up to request clarification or add additional context in comments.

2 Comments

holy cow, let me dive in
thanks for this - I am working on incorporating this answer - but I have a question here: stackoverflow.com/questions/52174924/keyof-but-for-arraystring, basically it's the same as the OP here but name is an array like name: ['bar'] instead of name: 'bar'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.