This is long and confusing, sorry. I’m modeling a large application’s architecture in TypeScript, consider the following layout:
Definition
enum ProjectKey {
Example1 = "example1",
}
class Project<TProjectKey extends ProjectKey, TApps extends App<any, any>[]> {
constructor(public key: TProjectKey, public apps: TApps) { }
}
class App<TName extends string, TSecrets extends Secrets = never> {
constructor(public name: TName, public port: number, public secrets?: TSecrets = undefined){ }
}
// boolean param is whether or not the secret is required.
type Secrets = Record<string, boolean>;
Implementation
type Example1Apps = [
App<"app1", App1Secrets>,
App<"app2">,
];
type App1Secrets = {
username: true;
password: true;
optionalSecret: false;
};
const Example1 = new Project<ProjectKey.Example1, Example1Apps>(ProjectKey.Example1, [
new App("app1", 1234, {
username: true,
password: true,
optionalSecret: false,
}),
new App("app2", 5678),
]);
type AllProjects = {
[ ProjectKey.Example1 ]: typeof Example1,
};
You can see that the goal here is to define a structure of (many) projects, their apps, and each app’s configuration. It creates some redundancy, sure, but the goal is strong-typing. This strongly-typed config structure is then used to build a configuration JSON which matches the structure.
For example, the config would need to define example1.app1.secrets.username = "..."
, but it cannot include a non-existent app or property, e.g. example1.app6...
and example1.app1.secrets.someOtherSecret
are invalid.
Configuration Definition
The config would look like this, and would be strongly typed:
type ClusterConfigProps = {
[ TProjectKey in keyof AllProjects ]: ProjectConfigProps<TProjectKey>;
};
type ProjectConfigProps<TProjectKey extends ProjectKey> = {
apps: {
[ TApp in AllProjects[TProjectKey]["apps"][number]["name"] ]: AppConfigProps<TProjectKey, TApp>;
};
};
// Below, how to get the app within "apps" whose name is TApp?
type AppConfigProps<TProjectKey extends ProjectKey, TApp extends AllProjects[TProjectKey]["apps"][{ name: TApp }] =
AllProjects[TProjectKey]["apps"][{ name: TApp }] extends { secrets: infer TSecrets }
? AppSecretsConfigProps<TSecrets>
: never;
type AppSecretsConfigProps<TSecrets extends Secrets> =
TSecrets extends undefined
? undefined
: {
[ TSecretKey in keyof TSecrets ]: TSecrets[TSecretKey] extends true
? { encryptedValue: string }
: { encryptedValue: string? }
};
Configuration Implementation
const config: ClusterConfigProps = {
example1: {
app1: {
secrets: {
username: "abcd",
password: "passw0rd",
// This one is optional, can be included or not.
optionalSecret: "shhh, it's optional",
},
// Note that app2 has no secrets (or other config), so it shouldn't be supplied.
},
// Other "projects"...
}
};
See it here in the TS Playground.
The main question here is how can I constrain an array’s items by some value of some key? Like I’ve attempted to do here … to “select” only the app whose name is the TApp
generic variable.
type X = AllProjects[TProjectKey]["apps"][{ name: TApp }]
So I would exepect:
type X = AllProjects[ProjectKey.Example1]["apps"][{ name: "app1" }]["secrets"];
To be:
"username" | "password" | "optionalSecret"
Is it possible?
20