Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript convert array to object with specified type

Tags:

typescript

I'm trying to convert an array of classes to a object that has it's class name as the object key. So;

I have an array of classes:

const renderers = [
    Battery,
    Kind
]

And I want to convert to an object like:

{
  Battery: Battery,
  Kind: Kind
}

To get to that, I use reduce to get the Object:

const convertedObject = renderers.reduce((acc, renderer) => {
    acc[renderer.name] = renderer;
    return acc;
}, {});

So convertedObject now has a type of {}. I want this type to be { Battery: typeof Battery, Kind: typeof Kind} (so that I can use keyof typeof to create a type with the string values.)

I know how to get the type of the array by doing type RendererType = typeof renderers[0], which gets me typeof Battery | typeof Kind. Then in the reducer I can do this:

renderers.reduce<{[x: string]: RendererType }>((acc, renderer) => {
    acc[renderer.name] = renderer;
    return acc;
}, {});

So now, convertedObject has a type of { [x: string]: typeof Battery | typeof Kind }, which is ok. But I rather it be the object I described earlier.

I tried to do

renderers.reduce<{[K in keyof RendererType]: RendererType }>((acc, renderer) => {
    acc[renderer.name] = renderer;
    return acc;
}, {});

But then I just get { prototype: typeof Battery | typeof Kind }

Is there a way to get the type that I would need with Typescript somehow?

like image 322
Jonathan Cammisuli Avatar asked Jan 24 '18 18:01

Jonathan Cammisuli


3 Answers

Sadly, this is currently impossible to do with arrays due to the fact that TypeScript does not hold the name property as a string literal in an object's prototype.

class Ball {}

const className = Ball.name; // resolves to just `string`, not `"Ball"`
console.log(className); // "Ball"

Here are the two solutions that I would go with if I was in your position.

#1 - Accept the renderers as an object instead of an array.

Although, I only advise you go with this solution if the order of your renderer classes does not matter. The JavaScript key-value shorthand would make this easy to incorporate for your needs.

class Ball {}
class Person {}

const paramForFunc = { Ball, Person };
type RendererNames = keyof typeof paramForFunc; // "Ball" | "Person"

#2 - Require renderers implement an interface with a property for the name

interface RendererConstructor {
    readonly alias: string;
}

class Foo {
    static alias = "Foo" as const;
}

class Bar {
    static alias = "Bar" as const;
}

function assertRenderers<T extends readonly RendererConstructor[]>(renderers: T): T[number]["alias"] {
    throw Error("unimplemented");
}

const foo = assertRenderers([Foo, Bar] as const); // "Foo" | "Bar"
like image 113
sno2 Avatar answered Oct 10 '22 07:10

sno2


Is the array fixed to two? If yes, then probably can do the below:

interface R { battery?: B; kind?: K; } // set fields as optional

renderers.reduce((acc, renderer) => {
  acc[renderer.name] = renderer;
  return acc;
}, {} as R); // strong type it manually 
like image 2
Chybie Avatar answered Nov 12 '22 21:11

Chybie


function b<K extends string>(...keys: K[]) : { [T in K]: string }
{
    const init = {} as { [T in K]: string }
    return keys.reduce((a, v) => ({ ...a, [v]: 'label'}), init);
}
var l = b('key1', 'key2');
console.log(l.key1, l.key2);

Maybe that's you want.

like image 2
Garry Xiao Avatar answered Nov 12 '22 22:11

Garry Xiao