1

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.

1
  • Your runner library makes no guarantees through the type system that the arguments given to run matches the parameters of fn. Commented Sep 15, 2022 at 13:45

1 Answer 1

2

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.

Playground

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you! Is there an alternative way to do the same without using generics?
No, I don't think so.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.