I am experimenting with ideas for an API config system, and would like some help on typing a config object.
Note: In terms of the API, this is a resources/endpoints config, with each resource/endpoint having a specified path which must contain certain parameters. I will talk more in terms of TypeScript, to simplify terminology and avoid confusion.
The basic problem is that I have a config object, where each of the properties has a nested property that must contain certain substrings. I have this schema object, which defines what substrings the nested property must contain (the substrings specified in this object are each wrapped in curly braces to create the actual substring):
const pathsSchema = {
foo: {
params: ["id"], // substring would be `{id}`
},
} as const;
(I generate a type from this object. I can't remember why I decided it needs to be an object instead of just a type. If they want to, answerers can ignore it and just use a type directly.)
The corresponding config object for this schema would be something like the following:
const config = {
foo: {
path: "foo?id={id}",
},
};
With a single substring, it is simple, as I can just type it with the following:
Path<param extends string> = `${string}{${param}}${string}`
type Paths<schema = typeof pathsSchema> = {
[name in keyof schema]: {
path: Path<schema[name]["param"]>
};
};
However, a similar approach with multiple substrings would require generating every possible permutation, which is obviously not a good idea.
Update: This is moot! See update at bottom of question.
So currently I have this generic type that resolves to true
if a string contains the required substrings:
type CheckPath<path extends string, params extends string[]> =
// A. If there is a first param, get it.
params extends [infer param extends string, ...infer rest extends string[]]
? path extends `${string}{${param}}${string}` // B. If path contains param...
? CheckPath<path, rest> // B. ...check next param...
: false // B. ...else, path is invalid.
: true; // A. There are no more params, so path is valid.
I then have the following framework. The defineConfig
helper is a pattern used to provide typing in a separate, "user"-provided config file; e.g. vite.config.ts
for Vite.
const defineConfig = (config: Paths) => config;
const config = defineConfig({
foo: {
path: "foo?id={id}",
},
});
Is there some way I can require that the object passed to defineConfig
passes checking with CheckPath
?
I do not like these kind of "validation" types, but I don't know if there's another way.
Update: This is moot: template literal types can be intersected! Yay TypeScript! (Thanks to Alex Wayne for pointing this out.)
So now my question boils down to: how do I go from this schema with a map of names-to-string-arrays to a map of names-to-intersections-of-template-literal-types?