1

I am working on some localization module, I have enabled TypesScript Json modules for this, currently I am trying to achieve the following usage:

base.json:

{
  "base": "",
  "greeting": "{{name}}",
  "extras": {
    "array": ["{{city}}", 0]
  }
}

en.json / nl.json

{
  "base": "hello",
  "greeting": "hello {{name}}",
  "extras": {
    "array": ["I live in {{city}}", 7]
  }
}

I want all files to implement the base so I did the following

import base from "./locale/base.json";
import en from "./locale/en.json";
import nl from "./locale/nl.json";

type Language = keyof typeof availableLanguages;

const availableLanguages: Record<"en" | "nl", typeof base> = {
  en,
  nl,
} as const;

export const setLanguage = <T extends Language>(lang: T) => {
  return availableLanguages[lang];
};

This is the bare minimum I got to work, however I want to get this usage:

const t = setLanguage("en")
t.base // hello
t.greeting({ name: "Daniell" }) // hello Daniell
t.extras.array({ city: "My city" }) // ["I live in My city", 7]

I was wondering how I can achieve something like this, I could use some help with specifically:

  • If I were to generate typings from my base.json by extracting the mustache notations to it's own interface, how I should I structure those to map them to each path and would this be the best approach?
  • How should I type the interface for the strings / arrays? since the example below wouldn't give me any intellisense
interface String {
  (...args: any): any;
}
  • There is [no way](https://stackoverflow.com/questions/19335983/can-you-make-an-object-callable) you can make a string, object or array callable in JS. So before thinking about getting IntelliSense you need to have a working solution. – Shlang Apr 12 '20 at 12:10
  • I'm not sure I understand the question, are you trying to base the typescript types from the base.json file? I'm not sure that's possible without code generation – Ben Winding Apr 12 '20 at 12:18
  • @Shlang it is possible using Proxy's, I'm more worried about the typings –  Apr 12 '20 at 13:01
  • @BenWinding I think you are right, I wonder what kind of output would be the easiest to work with because I somehow need to map the whole object path to a type –  Apr 12 '20 at 13:05

1 Answers1

0

While you cannot get the names of expected interpolated parameters you still can get a correct shape of the json (using --resolveJsonModule flag). So something like

type RecursiveProxified<T> = {
  [K in keyof T]: T[K] extends string
    ? (...values) => string
    : T[K] extends Array<string | number>
    ? (...values) => Array<string | number>
    : T[K] extends {}
    ? RecursiveProxified<T[K]>
    : never
};

can provide a basic typechecking and intelliSense capabilities.

const proxifyTranslations = <T>(translations: T): RecursiveProxified<T> => {
  // there should be actual implementation
  return translations as any; 
};

const availableLanguages = {
  en: json // json module
};

const setLanguage = (lang: keyof typeof availableLanguages) => {
  return proxifyTranslations(availableLanguages[lang]);
};

const t = setLanguage("en");

t.base(123); // string
t.extras.array(123); // Array<string | number>

Playground link

Shlang
  • 2,495
  • 16
  • 24