typescriptincompatibletypeerror

How to fix 'Type is missing properties from type' with generic object?


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.


Solution

  • 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