Consider a library which exports a run
function like below:
runner.ts
export type Parameters = { [key: string]: string };
type runner = (args: Parameters) => void;
export default function run(fn: runner, params: Parameters) {
fn(params);
}
And consider the following code in a separate file:
index.ts
import type { Parameters } from "./runner.ts";
import run from "./runner.ts";
type CustomParams = { hello: string };
function logGenericArgs(args: Parameters): void {
console.log(args);
}
function logHelloFromArgs(args: CustomParams): void {
console.log(args.hello);
}
run(logGenericArgs, { abc: "123" });
run(logHelloFromArgs, { hello: "123" }); /* Argument of type '(args: CustomParams) => void' is not assignable to parameter of type 'runner'.
Types of parameters 'args' and 'args' are incompatible.
Property 'hello' is missing in type 'Parameters' but required in type 'CustomParams'. ts(2345)
*/
Why does TypeScript complain about the different types, when they're perfectly compatible with each other? From my understanding, type Parameters
is a generic object with string
keys and string
values; CustomParams
's "hello" key perfectly fits the Parameters
's type signature.
How can I make the code in the "runner" library to accept a generic object type, and work nicely with other types that are compatible?
I do not want to use type unknown
or any
, as that's basically useless. I want the call signature of the run
function to express that args
is an object, but I do not want to limit the args
' type to that specific signature only.
I also do not want to specify the hello
key in type CustomParams
as optional, because that key should not be optional in the usage of the type CustomParams
- and I do not want to add the key hello
in type Parameters
because it is not required in every use case of the "runner" library.
TypeScript doesn't know that params
is supposed to be the parameters of fn
, but you can easily fix that but adding a generic parameter to associate the two:
type Params = { [key: string]: string };
type runner<P extends Params = Params> = (args: P) => void;
function run<P extends Params>(fn: runner<P>, params: P) {
fn(params);
}
Now when you call your custom params function:
run(logHelloFromArgs, { hello: "123" });
It's inferred as
function run<{
hello: string;
}>(fn: runner<{
hello: string;
}>, params: {
hello: string;
}): void
which is what we want.