Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
530 views
in Technique[技术] by (71.8m points)

generics - Map of heterogeneous functions in Typescript

I want to create a registry of different functions in Typescript.

For example I have several functions with different arguments types:

const func1 = (p: { x: number }) => {
  console.log("number: " + p.x);
};
const func2 = (p: { s: string }) => {
  console.log("string: " + p.s);
};

I create a registry with them and giving them names:

const map = {
  number: func1,
  string: func2
};

Then I want to infer following type:

{ funcName: "number", x: number } | { funcName: "string", s: string }

This type holds the information about function name in registry and its arguments type. I can do this automatically by following code:

type Keys = keyof typeof map;

// assume all functions have single input argument
type ArgByName<Key extends Keys> = Parameters<typeof map[Key]>[0];

type ArgExtendedByName<Key extends Keys> = ArgByName<Key> & { funcName: Key };

type ArgType = { [Key in Keys]: ArgExtendedByName<Key> }[Keys];

Finally I write a function to call functions from registry:

function Apply(args: ArgType) {
  const { funcName, ...funcArgs } = args
  map[funcName](funcArgs);
}

But I get the following error:

Argument of type "{ s: string; } | { x: number; }" is not assignable to parameter of type "{ x: number; } & { s: string; }".
  Type "{ s: string; }" is not assignable to type "{ x: number; } & { s: string; }".
    Property 'x' is missing in type '{ s: string; }' but required in type '{ x: number; }'.ts(2345)
index.ts(1, 21): 'x' is declared here.

I really cannot understand, why the input arguments of functions became this { x: number; } & { s: string; }, but not this { s: string; } | { x: number; }". Is it Typescript bug or my error? How does this problem can be solved?

Also here is a playground with all code above: https://codesandbox.io/s/sad-mccarthy-nvb47?fontsize=14&hidenavigation=1&theme=dark

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

This looks like another case of what I've been calling "correlated union types" (see microsoft/TypeScript#30581); you have multiple values of union types which are correlated to each other, but the compiler treats them as uncorrelated.

In general, if you have a value [x, y] of type [A, B] | [C, D], the compiler will end up treating x and y as independent variables of type A | C and B | D respectively. Therefore [x, y] is suddenly being treated like [A, B] | [A, D] | [C, B] | [C, D] with those impossible [A, D] and [C, B] cross-correlated terms in there. The compiler forgets the correlation, and then complains about having forgotten.

There's not much in the way of support for this sort of thing in TypeScript. Sometimes you can refactor such code into a generic function which behaves as you want, but in general you either need to duplicate code, like this:

function ApplyRedundant(args: ArgType) {
    if (args.funcName === "string") {
        const { funcName, ...funcArgs } = args;
        map[funcName](funcArgs);
    } else {
        const { funcName, ...funcArgs } = args;
        map[funcName](funcArgs);
    }
}

Or use something like a type assertion:

function ApplyAssert(args: ArgType) {
    const { funcName, ...funcArgs } = args;
    map[funcName](funcArgs as ArgByName<"string"> & ArgByName<"number">);
}

I usually recommend doing a type assertion. Note that I've asserted from the union to the intersection; that's because of a change introduced in TypeScript 3.3 to allow calling unions of functions with intersections of arguments. Before this change your call would just have been an error saying "you can't call this at all". Afterward it's saying "I don't know (or forgot) whether this function wants an ArgByName<"string"> or whether it wants an ArgByName<"number"> so you'd better pass me something that's both of them at the same time, so I know it will work no matter which one it is."

Playground link to code


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...