I would like to constrain a parameter type of a callback function that is inside an object like this:
makeObj({
items: [
{
value: "foo",
func(items) {}
},
{
value: "bar",
func(items) {}
}
]
});
I want to constrain the parameter items
of func
to "foo" | "bar"
. The values of value
can be any string though.
I already have this, but it obviously doesn't work:
interface MyObject<T extends string, Values extends readonly MyObject<T, Values>[]> {
value: T;
func: (items: Values) => void;
}
interface Items<T extends string, Data extends readonly MyObject<T, Data>[]> {
items: readonly [...Data];
}
function makeObj<T extends string, Data extends readonly MyObject<T, Data>[]>(opt: Items<T, Data>) {
return opt;
}
makeObj({
items: [
{
value: "foo",
func(items) {}
/* Error on "func":
Type '(items: MyObject<string, unknown>) => void' is not assignable to type '(items: unknown) => void'.
Types of parameters 'items' and 'items' are incompatible.
Type 'unknown' is not assignable to type 'MyObject<string, unknown>'.
*/
},
{
value: "bar",
func(items) {} // same error on "func"
}
]
});
Some background info:
I'm writing a program that has some "actions" defined. Each action is a function in an object to describe the function (give it a name, description, input type, output type, secondary input type). The secondary input relies on user provided data. Each item of this data has a name and defined type. Its value is provided by the end user. Depending on the value of other items an item can be displayed to the user or not. This last part is controlled by the callback function in question.
I have all this already set up with narrow type inference to not make any mistakes while writing each action but this narrow type inference breaks down regardless of which type the callback function has. Either because it can't infer the narrow type anymore, or because I get a type error (e.g. when I use object
as type for the parameter (I ultimately want to use an object as the parameter, not just a string)).
This narrow type inference works like this: typescript - Infer/narrow function argument from sibling property
Edit:
I would like a solution where I don't have the type embedded in the function parameter but as an interface/type that I can reference like:
interface Magic<MagicParam> {
items: MagicParam;
}
makeObj<MagicParam>(opt: Magic<MagicParam>) {
return opt;
}
Magic
can be an interface or type and can have any number of type parameters. makeObj
too can have any number of type parameters.
Your makeObj()
function could be generic in the union of string literal types corresponding to the value
properties of the function parameter's items
property. If we call that generic type parameter K
, and if you pass in, for example, {items: [{value: "x"},{value: "y"},{value: "z"}]}
(ignoring func
for now), then K
should be the "x" | "y" | "z"
Then you can express the argument to makeObj()
in terms of K
. If we call that type Opt<K>
, we can write it like this:
interface Opt<K extends string> {
items: Array<{ value: K, func: (items: K) => void }>
}
That means, given K
of value
properties, we want the items
property of Opt<K>
to be an array, where the elements of the array are objects with a value
property of type K
, and a func
callback property whose input is type K
.
Let's make sure that's what you want, by evaluating Opt<"foo" | "bar">
(and using conditional type inference and a mapped type to coax the compiler into displaying details of the structure):
type Test = Opt<"foo" | "bar"> extends
infer O ? { [K in keyof O]: O[K] } : never
/* type Test = {
items: {
value: "foo" | "bar";
func: (items: "foo" | "bar") => void;
}[];
} */
Looks good.
Anyway, as mentioned above, makeObj
will be generic in K
and take a parameter of type Opt<K>
:
function makeObj<K extends string>(opt: Opt<K>) {
return opt;
}
And let's see if it works:
const obj = makeObj({
items: [
{
value: "foo",
func(items) {
// (parameter) items: "foo" | "bar"
}
},
{
value: "bar",
func(items) {
// (parameter) items: "foo" | "bar"
}
}
]
});
// const obj: Opt<"foo" | "bar">
Looks good too. The compiler infers that K
is "foo" | "bar"
, and then contextually types the items
callback parameters of the func
property to also be "foo" | "bar"
. And the obj
that comes out is of the desired Opt<"foo" | "bar">
type.