Cross language enum sharing
2 min read

Cross language enum sharing

If you are working in a multi platform project, where each platform may be using a different language, you will eventually run into a situation where you want to share constants or enums between platforms. If you are running everything on JS/TS, you can just create an NPM package with your 'SDK', containing whatever you need. However, what if you are using, as was the case for us, Dart/Flutter for the mobile part of your app, while the backend ran on Node.js?

The answer, without giving too much away, is code generation. Or, in less fancy terms: spitting out a bunch of strings that look like native code.

In this example, the backend is our singe source of truth. It's where we define the enums to share, and which will in turn share them to the rest of the app.

Let's say we have an enum ImageSize that we would like to share:

export enum ImageSize {
    THUMBNAIL = 'thumb',
    SMALL = 'small',
    MEDIUM = 'medium',
    LARGE = 'large',
}

Using a helper function we will feed our enum(s) into another function, one per output language, that will convert it to native code. We're also passing a newline character, so we could potentially return the text with CRLF line breaks for Windows environments.

Since enums are a bit tricky in TypeScript in that they do not really exist during compile time, we can't just pass the Enum Type in and work with that as a Generic (or at least I was not able to get it to work in a way that offered all I needed), so we're passing in the values we need directly.

enumName: the name that the generated enum/class will have
enumKeys: the enum keys as returned by Object.keys(ENUM)
newline: \n by default, but could be \r\n for CRLF format
enumValues: optional array of values for String Enums (in our special case we agreed to assign upper case strings to all enum keys unless otherwise defined)

Dart

Dart, even with the version 2.15 changes, still does not support String Enums. There are packages on pub.dev for this, but we did not want to add another dependency to the project. Instead, we just fake it by using classes with static const values and a values() function that will return an Iterable just like an enum.

export function enumToDart(enumName: string, enumKeys: string[], newline = '\n', enumValues?: string[]): string {
  const keysLowerCase = enumKeys.map((k) => k.toLowerCase());
  const keyValues = enumValues || enumKeys.map((k) => k.toUpperCase());
  const keys: string[] = enumKeys.map((k, idx) => `  static const ${keysLowerCase[idx]} = "${keyValues[idx]}";`);
  const s: string[] = [
    `class ${enumName} {`,
    ...keys,
    ``,
    `  static Iterable<String> values() sync* {`,
    `    for (String s in [${keysLowerCase}])`,
    `    yield s;`,
    `  }`,
    `}`,
    ``,
  ];
  const merged = s.join(newline) + newline;
  return merged;
}

which will return when called like enumToDart('ImageSize', Object.keys(ImageSize), newline, Object.values(ImageSize))

class ImageSize {
  static const thumbnail = "thumb";
  static const small = "smallL";
  static const medium = "medium";
  static const large = "large";

  static Iterable<String> values() sync* {
    for (String s in [thumbnail,small,medium,large])
    yield s;
  }
}

Typescript

Creating the same in Typescript is rather more straight forward:

export function enumToTS(enumName: string, enumKeys: string[], newline = '\n', enumValues?: string[]): string {
  const keyValues = enumValues || enumKeys.map((k) => k.toUpperCase());
  const keys: string[] = enumKeys.map((k, idx) => `  ${k} = '${keyValues[idx]}',`);
  const s: string[] = [`export enum ${enumName} {`, ...keys, `}`, ``];
  const merged = s.join(newline) + newline;
  return merged;
}

which will return when called like enumToTs('ImageSize', Object.keys(ImageSize), newline, Object.values(ImageSize))

export enum ImageSize {
    THUMBNAIL = 'thumb',
    SMALL = 'small',
    MEDIUM = 'medium',
    LARGE = 'large',
}

which is just the same as the original enum.

These functions return values can now, for example, be pushed into a Readable() and then pipe()'d to the Response of an Express endpoint. And your frontend devs could now regularly pull the enums for local development, e.g. via wget:

wget "https://.../enum?lang=dart" -O enum.dart