There are several solutions to your problem that are equally valid but progressively enhance the type safety.
The first one is the trivial case you presented: (x: any) => any. Despite the "never use any" rule of thumb, a judgemental use of any can be a valid choice. If all you need is to tell the compiler that the function accepts a single argument of any type, then why not?
The second one is more sound and was already proposed by fjc in the comment:
Why not use a generic like this: type SingleParamFunction = (arg: T) => T; const foo: SingleParamFunction = (x) => x + 1;?
This solves the contravariance issue, but the compiler will complain about the signatures of the transform methods because the union will be passed through, so the x and the return type will be inferred as a union (for example, string | number):
interface Foo<I> {
input: I;
transform: SingleParamFunction<I>;
}
const foos: Foo<string | number>[] = [
{
input: 1,
transform: (x) => x + 1, //Type 'string' is not assignable to type 'number'.(2322)
},
{
input: 'hello',
transform: (s: string) => s + ' world', //Type 'string | number' is not assignable to type 'string'.
},
];
for (const foo of foos) {
console.log(foo.transform(foo.input));
}
This is, however, not the solution's problem. Because foos is typed as a mutable array, the compiler does not know what the type of input or transform's x and return type in a given member is, only that it should be string | number.
If you remove the type annotation, you will see, however, that the type of the array is correctly inferred. This should give you a hint of what you need to check for and leads us to a third solution:
({
input: number;
transform: (x: number) => number;
} | {
input: string;
transform: (s: string) => string;
})[]
So, how would you do it? Mapped types to the rescue! You need to define an "array of Foo's where for each type in possible input types there possibly is a member that is a Foo of this type". Here is how such a type could look like (with a help of key remapping):
type validator<I> = Array<{ [T in I as number]: Foo<T> }[number]>;
And voila:
//all ok, valid assignment
const bars: validator<string | number> = [
{
input: 1,
transform: (x: number) => x + 1
},
{
input: 'hello',
transform: (s: string) => s + ' world'
},
];
const invalid : validator<string|number> = [
{
input: "nope", //Type 'string' is not assignable to type 'number'.(2322)
transform: (x: number) => x + 1
},
{
input: 'hello',
transform: (s: string) => Infinity //Type 'number' is not assignable to type 'string'.(2322)
},
];
Playground