Reputation: 901
I have a class where I want the end-user to be able to specify a map of (arbitrary, user-defined) names for (arbitrary, user-defined) configuration objects that will type different properties and methods inside the class through generics.
My current working implementation looks like this:
// Configs should be objects. Primitives not allowed
// Incidentally, is there a better way to constrain a generic to be an object?
type ModConfig = Record<string, any>
type Operation<N extends string, C extends ModConfig> = {
mod: N,
config: C,
status: string,
}
class Controller<N extends string, M extends Record<string, ModConfig>> {
activeModule: N;
private opsByMod: Operation<N, M[N]>[] = []
private configs: { [K in N]?: M[K] } = {}
constructor(startMod: N) {
this.activeModule = startMod
}
addConfig<K extends N>(mod: K, config: M[K]) {
this.configs[mod] = config;
}
}
// Sample usage
interface ModAConfig {
aNumber: number,
aString: string,
}
interface ModBConfig {
bBoolean: boolean,
bObject: {
bNested: string,
}
}
type myModules = 'moduleA' | 'moduleB'
const cInstance = new Controller<myModules, { moduleA: ModAConfig, moduleB: ModBConfig }>('moduleA')
While this works, I would like to remove the generic param N
as it is redundant if I could just use keyof M
. Like this:
class Controller<M extends Record<string, ModConfig>> {
// This is no longer 'string' but 'string | number | symbol'
activeModule: keyof M;
// TS Error:
// Type 'keyof M' does not satisfy the constraint 'string'.
// Type 'string | number | symbol' is not assignable to type 'string'
private opsByMod: Operation<keyof M, M[keyof M]>[] = []
// This also now takes not only 'string' but 'string | number | symbol' as keys
private configs: { [K in keyof M]?: M[K] } = {}
constructor(startMod: keyof M) {
this.activeModule = startMod
}
// Same deal
addConfig<K extends keyof M>(mod: K, config: M[K]) {
this.configs[mod] = config;
}
}
However as I commented above, keyof M
produces string | number | symbol
. This seems to be because M extends Record<string, ModConfig>
which I guess is interpreted as numbers/symbols potentially being added as keys by the extension being supplied as parameter for M.
Is there a way to do this so that M is constrained strictly to only being keyed by strings so I can just substitute N
with keyof M
?
Upvotes: 0
Views: 1689
Reputation: 42188
You can use this type to extract only keys of M
which are strings
.
type StringKeys<T> = Extract<keyof T, string>
Everywhere in your code that you were using keyof M
, replace it with StringKeys<M>
.
Now it's still theoretically possible for M to contain number keys, but those keys can't be used for setting the activeModule
or getting an Operation
.
class Controller<M extends Record<string, ModConfig>> {
activeModule: StringKeys<M>;
private opsByMod: Operation<StringKeys<M>, M[StringKeys<M>]>[] = []
private configs: { [K in StringKeys<M>]?: M[K] } = {}
constructor(startMod: StringKeys<M>) {
this.activeModule = startMod
}
addConfig<K extends StringKeys<M>>(mod: K, config: M[K]) {
this.configs[mod] = config;
}
}
Upvotes: 4