type StringKey = 'key1' | 'key2' | 'key3.key3_1' | 'key3.key3_2'
Note that, for nested values, we’re generating the key using all of the keys of its parent (until we reach the root), separated by a `.`. This is a general convention followed in the JavaScript ecosystem and supported by libraries such as lodash.
Next, we can utilize this type within the `StringsContext` file and take advantage of TypeScript.
The following script should read the YAML file and generate the types:
import fs from "fs"; import path from "path"; import yaml from "yaml";
/** * Loops over object recursively and generate paths to all the values * { foo: "bar", foo2: { key1: "value1", key2: "value2" }, foo3: [1, 2, 3] } * will give the result: * * ["foo", "foo2.key1", "foo2.key2", "foo3.0", "foo3.1", "foo3.2"] */ function createKeys(obj, initialPath = "") { return Object.entries(obj).flatMap(([key, value]) => { const objPath = initialPath ? `${initialPath}.${key}` : key;
if (typeof value === "object" && value !== null) { return createKeys(value, objPath); }
return objPath; }); }
/** * Reads input YAML file and writes the types to the output file */ async function generateStringTypes(input, output) { const data = await fs.promises.readFile(input, "utf8"); const jsonData = yaml.parse(data); const keys = createKeys(jsonData);
You can put this script in `scripts/generate-types.mjs` and run `node scripts/generate-types.mjs`. Furthermore, you should see `src/strings.types.ts` being written with the following content:
The script, in its current form, doesn’t handle all of the use cases/edge cases. You can enhance it, when required, and customize it according to your use case.
Now we can update the `StringsContext.tsx` to utilize the generated type `StringKeys`.
import React, { createContext } from "react"; import has from "lodash.has"; import get from "lodash.get"; import mustache from "mustache";
+ import type { StringKeys } from "./strings.types";
+ export type StringsMap = Record<StringKeys, string>;
- const StringsContext = createContext({} as any); + const StringsContext = createContext<StringsMap>({} as any);
After this change, you should be able to utilize autocomplete and validation for presence strings using TypeScript.
Moreover, you can integrate the string generation into your build system. This will automate the generation of types whenever there is a change in the `strings.yaml` file. I've done it here using a vitejs plugin.
Conclusion
I hope you find this useful and will use it as a starting point for your own implementation. For those who missed Part 1, again, you can find it here: Externalizing Strings In React – Part 1.