Consider the following type definitions in TypeScript:
enum Environment {
Local = 'local',
Prod = 'prod'
}
type EnvironmentConfig = {
isCustomerFacing: boolean,
serverUrl: string
}
type DefaultBaseConfig<T> = {
default: T
}
type EnvironmentBaseConfig<T> = {
[key in Environment]: T
}
type BaseConfig<T> = DefaultBaseConfig<T> | EnvironmentBaseConfig<T>;
// const baseConfig: ??? = {
const baseConfig: BaseConfig<Partial<EnvironmentConfig>> = {
default: {
isCustomerFacing: false
},
local: {
serverUrl: 'https://local.example.com'
},
prod: {
isCustomerFacing: true
}
};
Notice the object at the end with const baseConfig: ??? and the Partial<> on the next line. What I really want is for baseConfig to allow each Environment-keyed property to be a Partial<EnvironmentConfig>, and to allow the default property to be the same, but also require that the intersection between default and each environment local and prod in turn must be a full (not Partial<>) EnvironmentConfig, thus fulfilling the requirements of some type along the lines of BaseConfig.
In the example case here, local would be valid because when combined with default it has both properties. But, prod would not be valid because no serverUrl has been declared between the intersection of default and prod.
Obviously, at a later time, this config object will be merged conditionally in some code that takes a BaseConfig and an environmentName and returns an EnvironmentConfig, and it will be guaranteed to work at runtime if it is given a static config object that has been inspected by Typescript to comply with the desired constraints.
I have been puzzling away at this for a while and am stuck. I know how to do conditional types and type constraints such as T extends U ? T : never, but can't seem to figure out how and where to apply that to this scenario.
How can I achieve this goal?
Here is my best attempt so far, but, of course it doesn't work:
type SplitWithDefault<
TComplete,
TDefault extends Partial<TComplete>,
TSplit extends { default: TDefault, [key: string]: Partial<TComplete> }
> = { default: TDefault }
& { [P in keyof Omit<TSplit, 'default'>]: (TSplit[P] & TDefault) extends TComplete ? TSplit[P] : never };