I’m trying to create a type structure which defines a set of projects (e.g. software projects), each with one or more apps (e.g. API and UI), each of which can have some strongly-typed configuration properties (secrets
, env
vars).
When I try what I have below, I get the errors:
Type ‘TProjectKey’ cannot be used to index type ‘AllProjects’.
Type ‘”apps”‘ cannot be used to index type ‘AllProjects[TProjectKey]’.
Type ‘TAppType’ cannot be used to index type ‘AllProjects[TProjectKey][“apps”]’.
Type ‘”secrets”‘ cannot be used to index type ‘AllProjects[TProjectKey][“apps”][TAppType]’.
My attempt is below — see incline comments.
enum ProjectKey {
Project1 = "projectOne",
Project2 = "projectTwo",
}
enum AppType {
API = "api",
UI = "ui",
}
type Project<TProjectKey extends ProjectKey> = {
key: TProjectKey;
name: string;
apps: Partial<{ [ TAppType in AppType ]: App<TAppType> }>;
}
type App<TAppKey extends AppType = AppType, TSecretKeys extends string = never> = {
type: TAppKey;
name: string;
secrets: Secrets<TSecretKeys>;
}
type OptionalSecret = {
defaultValue?: string;
};
type RequiredSecret = {
required: true;
};
type Secret = OptionalSecret | RequiredSecret;
type Secrets<TSecretKeys extends string> = { [ K in TSecretKeys ]: Secret };
// Define a project.
const Project1: Project<ProjectKey.Project1> = {
key: ProjectKey.Project1,
name: "Project One",
apps: {
api: {
type: AppType.API,
name: "Project One API",
secrets: {
"SECRET_1": { required: true },
"SECRET_2": { defaultValue: "abc" },
},
},
},
};
// Roll up all projects into a mapped type.
type AllProjects = {
[ ProjectKey.Project1 ]: typeof Project1;
};
// Define a configuration which describes all of the projects.
type Config = {
[ TProjectKey in ProjectKey ]?: ProjectConfigProps<TProjectKey>;
};
type ProjectConfigProps<TProjectKey extends ProjectKey> = {
apps: { [ TAppType in AppType ]: AppConfigProps<TProjectKey, TAppType> };
};
// Error occurs below.
type AppConfigProps<TProjectKey extends ProjectKey, TAppType extends AppType> = {
secrets?: {
[ TSecretKey in keyof AllProjects[ TProjectKey ][ "apps" ][ TAppType ][ "secrets" ] ]:
AllProjects[ TProjectKey ][ "apps" ][ TAppType ][ "secrets" ][ TSecretKey ] typeof RequiredSecret
? { value: string }
: { value?: string }
};
};
// Usage -- the goal is to strongly type the configuration object so that each required secret must be provided, and each optional one may or may not.
const config: Config = {
"projectOne": {
apps: {
api: {
secrets: {
SECRET_1: { value: "some_required_value" },
SECRET_2: { value: "some_default_value" },
// Or SECRET_2 can just not be supplied.
}
},
// How to make this not required if there are no required secrets?
ui: {
secrets: undefined,
},
},
},
};
Check it out in the playground.