Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

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

typescript inference without enumerating all possibilities

I would like to know if there is a solution for the following problem : I have two functions that accept different input parameters, I have an object mapping each function to a string.

I define a type that is a union type of object containing the key of one function in the map and a value matching the function parameters type. Given an object with that type, I would like to retrieve the corresponding function from the map object and call it with the value but typescript does not do the type inference.

const f1 = (value: number) => value;
const f2 = (value: string) => value;

const map = {
    f1,
    f2
};

type MyType = {
    fun: "f1";
    value: Parameters<typeof map["f1"]>;
} | {
    fun: "f2";
    value: Parameters<typeof map["f2"]>;
};

// does not work
const test1 = (b: MyType) => {
    map[b.fun].apply(null, b.value);
};

// works
const test2 = (b: MyType) => {
    if (b.fun === "f1") {
        map[b.fun].apply(null, b.value);
    } else {
        map[b.fun].apply(null, b.value);
    }
};

Is it possible to do something like this without having to enumerate all possibilities in a switch or if/else ?

See Question&Answers more detail:os

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

1 Answer

0 votes
by (71.8m points)

You've run into TypeScript's less-than-stellar support for what I've been calling correlated record types. Right now it sees that b.fun is a union type "f1" | "f2", and that b.value is a union type [string] | [number], and it treats these as uncorrelated. Normally a union of functions can only be called with an intersection of its parameters, which in this case would be [string] & [number]. Since [string] | [number] is not assignable to [string] & [number], the compiler complains. In fact no value is assignable to [string] & [number], since its first element would need to be a string & number which is equivalent to never.

In order for the compiler to verify that map[b.fun].apply(null, b.value) is type safe, it would essentially need to analyze the code multiple times, one for each possible type of b. You can make this happen explicitly by duplicating the code in a switch/case or if/else statement, but it doesn't happen automatically (which makes sense because it would lead to exponentially increasing compile times as the number of union-typed values in code increases), and you can't even ask the compiler to do it on an opt-in basis.

For now, the only reasonable workaround is to accept that you know more than the compiler about the type safety of the code, and to use a type assertion or the equivalent to tell the compiler not to worry about it, like this:

type MapVals = typeof map[keyof typeof map];
type LooselyTypedMapFunction = (...x: Parameters<MapVals>) => ReturnType<MapVals>
const test1 = (b: MyType) => {
  (map[b.fun] as LooselyTypedMapFunction).apply(null, b.value);
};

Here the type LooseLyTypedMapFunction ends up taking the types of f1 and f2 and blurring them into a single function that accepts and produces string | number. And we use the type assertion map[b.fun] as LooselyTypedMapFunction to lie a little bit to the compiler. We're telling it that map[b.fun] will accept a string and it will also accept a number. This is false, of course, but will not run into a problem as long as you pass in the correlated b.value list and not some random thing like `[Math.random()<0.5 ? "oops" : 123]).

That type assertion might be more work than you want to do, but at least it might catch if you passed anything too crazy into map[b.fun], since [string] | [number] will at least prohibit [{foo: string}]. You can use a less safe assertion like this:

const test2 = (b: MyType) => {
  (map[b.fun] as Function).apply(null, b.value);
};

which doesn't require type juggling of a LooselyTypedMapFunction but will also accept (map[b.fun] as Function).apply(null, [{foo: 123}]);. So be careful.


Okay, hope that gives you some direction. Good luck!

Playground link to code


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...