I'm not sure how I might precisely describe my goal in words. Here is some code to explain what I would like to do:
type validTypes = string | number
type item = Record<string, validTypes>
interface Test extends item {
someint: number;
something: string;
}
let test: {
[P in keyof Test]: (arg: Test[P]) => boolean
} = {
someint: (arg) => arg > 0,
something: (arg) => arg == "hi"
}
I'm trying to make the mapped type [P in keyof Test]: (arg: Test[P]) => Test[P]
work, however, it's giving me the following error:
Type '{ someint: (arg: number) => boolean; something: (arg: string) => boolean; }' is not assignable to type '{ [x: string]: (arg: validTypes) => boolean; someint: (arg: number) => boolean; something: (arg: string) => boolean; }'.
Property 'someint' is incompatible with index signature.
Type '(arg: number) => boolean' is not assignable to type '(arg: validTypes) => boolean'.
Types of parameters 'arg' and 'arg' are incompatible.
Type 'validTypes' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.
This is quite confusing to me, as [P in keyof Test]: Test[P]
(without the function) seems to work. The error seems to appear when I try to use Test[P]
as a parameter in the function type.
Unfortunately the type of test
(which you can inspect via IntelliSense) is:
let test: {
[x: string]: (arg: ValidTypes) => boolean;
someint: (arg: number) => boolean;
something: (arg: string) => boolean;
}
And this is not a valid type. Indeed, if you tried to create your own variable of that type explicitly, the compiler would complain:
declare let test2: {
[x: string]: (arg: ValidTypes) => boolean;
someint: (arg: number) => boolean; // error!
//^^^^^ <-- not assignable to string index type '(arg: ValidTypes) => boolean'.
something: (arg: string) => boolean;
//^^^^^^^ <-- not assignable to string index type '(arg: ValidTypes) => boolean'.
};
The problem is that Item
, and thus Test
, has an index signature which gets mapped according to the same rule as the individual someint
and something
properties. The index signature property becomes (arg: string | number) => boolean
, meaning that every individual property needs to be a function that accepts an argument of type string | number
. But your someint
and something
properties become functions that only accept number
and string
respectively. With --strictFunctionTypes
enabled (part of the --strict
suite of compiler options), functions need to be contravariant in their parameter types (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript). So if you widen the function parameter type you narrow the function type. But the type mapping you apply is unfortunately covariant, and it produces an invalid type to which no value will be seen as assignable.
If you're only planning to access the someint
and something
properties of test
then you don't really care about or want an index signature at all. Indeed I'm not sure you want an index signature in Test
. So one way to proceed is to drop the index signature entirely:
type Satisfies<T, U extends T> = U;
type Test = Satisfies<Item,
{
someint: number,
something: string
}>;
Here Test
has no index signature; it's just {someint: number, something: string}
. I've created a Satisfies
helper type which acts kind of like a type-level satisfies
operator. The above compiles, so Test
is compatible with Item
, but you're not extending it. And so the following compiles:
let test: {
[P in keyof Test]: (arg: Test[P]) => boolean
} = {
someint: (arg) => arg > 0,
something: (arg) => arg == "hi"
} // okay
Or, if you need Test
to have the index signature you can build it in two parts:
interface TestWithoutIndexSignature {
someint: number,
something: string
}
interface Test extends Item, TestWithoutIndexSignature { }
And then make sure test
maps over TestWithoutIndexSignature
instead of Test
:
let test: {
[P in keyof TestWithoutIndexSignature]:
(arg: TestWithoutIndexSignature[P]) => boolean
} = {
someint: (arg) => arg > 0,
something: (arg) => arg == "hi"
} // okay
Finally, if you can't change Test
at all, then you can compute TestWithoutIndexSignature
from it, using key remapping in mapped type to filter out string
from the keys:
type TestWithoutIndexSignature =
{ [K in keyof Test as string extends K ? never : K]: Test[K] }
/* type TestWithoutIndexSignature = {
someint: number;
something: string;
} */
let test: {
[P in keyof TestWithoutIndexSignature]:
(arg: TestWithoutIndexSignature[P]) => boolean
} = {
someint: (arg) => arg > 0,
something: (arg) => arg == "hi"
} // okay
The particular approach is up to you, but the goal here is not to let test
have an index signature, since it only hurts you.