Jonathan Cammisuli
Jonathan Cammisuli

Reputation: 481

Typescript convert array to object with specified type

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?

Upvotes: 14

Views: 9867

Answers (4)

sno2
sno2

Reputation: 4173

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"

Upvotes: 1

Garry Xiao
Garry Xiao

Reputation: 242

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.

Upvotes: 3

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249476

I'm afraid what you want is not possible to convert an array to an object type having as property names the names of the types, at least not without manually specifying the names again (as suggested in another answer). You could start from the other end, have an object that conforms to the desired shape and convert it to an array:

const renderersObject = {
    Battery, // This is the same as Battery: Battery, so we don't have o specify it twice
    Kind
};

And then you can convert it to an array using Object.value (or a polyfill for it)

const renderers = Object.values(renderersObject);

Upvotes: 0

Jecfish
Jecfish

Reputation: 4189

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 

Upvotes: 2

Related Questions