I have two properties on an object being passed into a function. I want to constrain the deps
property on one object's properties to be a key of the other object.
E.g.:
const config = {
modules: {
bar: {
baz: true,
deps: ['foo', 'notADep'],
factory: () => Promise.resolve(() => {}),
},
},
services: {
foo: { factory: () => Promise.resolve(() => {}) },
},
};
In the above config, foo
is valid, but notADep
is not valid.
Here's my code:
function createApp<T extends Config>(config: T) {}
export type Service<T> = {
factory: () => Promise<() => void>;
deps?: (keyof T & string)[] | undefined;
}
export type Services<T = any> = Partial<Record<keyof T, Service<any>>>;
export type Modules<T = any> = Record<string, {
baz: boolean,
} & Service<keyof T>>
interface Config {
modules: Modules;
services: Services;
}
I'm getting an error on the config
parameter that
Argument of type '{ modules: { bar: { baz: boolean; deps: string[]; factory: () => Promise<() => void>; }; }; services: { foo: { factory: () => Promise<() => void>; }; }; }' is not assignable to parameter of type 'Config'.
Types of property 'modules' are incompatible.
Type '{ bar: { baz: boolean; deps: string[]; factory: () => Promise<() => void>; }; }' is not assignable to type 'Modules<any>'.
Property 'bar' is incompatible with index signature.
Type '{ baz: boolean; deps: string[]; factory: () => Promise<() => void>; }' is not assignable to type '{ baz: boolean; } & Service<string | number | symbol>'.
Type '{ baz: boolean; deps: string[]; factory: () => Promise<() => void>; }' is not assignable to type 'Service<string | number | symbol>'.
Types of property 'deps' are incompatible.
Type 'string[]' is not assignable to type '("toString" | "valueOf")[]'.
Type 'string' is not assignable to type '"toString" | "valueOf"'.
CodePudding user response:
I don't know if I understand you correctly but you could probably do something like this playground
export type Factory = {
factory: () => Promise<() => void>;
}
export type Services<T extends string[] = []> = { [K in T[number]]: Factory };
export type Dependencies<T extends string[] = []> = { deps: T, }
export type Modules<ModuleKeys extends string[] = [], Context extends object = {}> =
{ [M in ModuleKeys[number]]: Factory & Context }
interface Config<ModuleKeys extends string[] = [], ServiceKeys extends string[] = [], Context extends object = {}> {
modules: Modules<ModuleKeys, Context & Dependencies<ServiceKeys>>;
services: Services<ServiceKeys>;
}
const config: Config<["bar"], ["foo"], { baz: boolean }> = {
modules: {
bar: {
baz: true,
deps: ['foo', 'notADep'], // error!
factory: () => Promise.resolve(() => { }),
},
},
services: {
foo: { factory: () => Promise.resolve(() => { }) },
},
};
Edit: So the problem with your solution are these lines:
export type Service<T> = {
factory: () => Promise<() => void>;
deps?: (keyof T & string)[] | undefined;
}
With (keyof T & string)[]
you are telling typescript that every key of T is valid that is a string. Everything in Typescript or Js is an object. For example
keyof number & string
will resolve to "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
. This will also happen if you use a string, object...
If you only want to get the a union of all used keys. You should use a tuple and indexing it with tuple[number] to receive nothing else but the values you want.
//e.g.
type Tuple = ["a","b"]
type Union = Tuple[number] // "a"|"b"
So why does this work? Remember everything is an object? An array is an object aswell and but all the keys for the values are numbers.
So with Tuple[number]
you are telling typescript to get all values with a number key.
Ps: You can also do Tuple[string]
to get all method on an array
My english is trash, so if you don't understand a thing, just ask and I'll try to help you
CodePudding user response:
You can achieve this by using the builder patter, Vue does this to infer props types.
It relies on inferred generic types to reuse them and self constrain the given arguments.
M
and S
refers to the properties modules
and services
of the given parameter
export type Factory = {
factory: () => Promise<() => void>;
}
type Service<M, S> = {
factory: () => Promise<() => void>
// deps are any keys of the modules or services
deps?: (keyof S | keyof M)[]
}
type Module<M, S> = {
baz: boolean
} & Service<M, S>
type Config<M, S> = {
modules: {
// For every property of M, inform it's a module with the services context (S)
[K in keyof M]: Module<M, S>
}
services: {
[K in keyof S]: Service<M, S>
};
}
// The function will automatically infer the given types
function createApp<M, S>(config: Config<M, S>): Config<M, S> {
return config
}
createApp({
modules: {
bar: {
baz: true,
// (property) deps?: ("bar" | "foo" | "plop")[] | undefined
deps: ['foo', 'notADep'], // error!
// ^ Error is located on the string, not the whole line
factory: () => Promise.resolve(() => { }),
},
plop: {
baz: true,
deps: ['foo', 'notADep'], // error!
factory: () => Promise.resolve(() => { }),
},
},
services: {
foo: {
factory: () => Promise.resolve(() => { }),
// (property) deps?: ("bar" | "foo" | "plop")[] | undefined
deps: [],
},
bar: {
factory: () => Promise.resolve(() => { }),
},
},
});