Dynamically access const object literal in TypeScript

Viewed 160

I'm working with a TypeScript library that generates TypeScript code, called ts-proto

This generated code looks like this:

    //BasicMessage.ts

    export interface BasicMessage {
      id: Long;
      name: string;
    }

    export const BasicMessage = {
     encode(message: BasicMessage) : Writer {
        ...
     }

     fromJSON(object: any): BasicMessage {
        ...
     }
   }

and

   // BasicMessagePlus.ts
  
   export interface BasicMessagePlus {
      id: Long;
      name: string;
      email: string;
   }

   export const BasicMessagePlus = {
     encode(message: BasicMessagePlus) : Writer {
        ...
     }

     fromJSON(object: any): BasicMessagePlus {
        ...
     }
   }

Since this code is generated, I can't change it. What I need to do is create a method that takes a type name and an object and encodes it, something like this:

   function encode(typeName: string, object: any): Writer {
      import(`/path/to/${typeName}`);
      return <typeName>.encode(<typeName>.fromJSON(object));
   }

   let writer1 = encode("BasicMessage", { id: 1, name: "Fake" });
   let writer2 = encode("BasicMessagePlus", { id: 1, name: "Fake", email: "fake@fake.com" });

I've tried all type of tricks using eval and globalThis, but I just can't find a combination that lets me do what I'm trying to do. Thanks for your help!

1 Answers

Dynamic imports like this are pretty much impossible for typescript to reason about. What if this function gets passed "ThisDoesNotExist"?

So you have to import all the available items generated this way into one object that can be used to look them up.

import { BasicMessage } from './path/to/BasicMessage'
import { BasicMessagePlus } from './path/to/BasicMessagePlus'
const encodables = { BasicMessage, BasicMessagePlus }

Now you can constrain typeName to a keyof that object, and everything works:

function encode(
  typeName: keyof typeof encodables,
  data: any
): string {
  const encoder = encodables[typeName]
  return encoder.encode(encoder.fromJSON(data))
}

That third line is a mouthful. Basically, it gets the type of the argument of the encode method of the chosen encoder.

Playground


Note that data here is of type any, mainly because that's how your fromJSON method is typed. If you want to enforce a certain type as the second argument according to which encoder was chosen, then this gets a lot more complicated.

Related