-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support overload resolution with type union arguments #14107
Comments
I can't find other issues where this discussion's been had, but I'll try to give some context. The single-parameter case always tends to be the most frustrating one for people, and the most obvious one to fix. The problem occurs when you have multiple overloads. For example, suppose you had the following overloads interface Foo {
bar(s1: string, s2: string): void;
bar(n1: number, n2: number): number;
} and you tried calling with the following: declare var sn1: string | number;
declare var sn2: string | number;
declare var foo: Foo
foo(sn1, sn2); Should our call to So we could come up with a way of trying to collapse overloads that differ by exactly one parameter into unions and retying the overload process, but I think our concerns are
|
Ok. I think a more general way to look at it would be this: For your example, it would look for overloads for each of The problem space would grow exponentially, but functions generally don't have a large number of parameters with a large number of types in each union. It could be short-circuited to ensure that every necessary type exists at each positional parameter first, before computing permutations. For generic parameters short-circuiting might not always be possible. |
#1805 was the prior incarnation of this |
@DanielRosenwasser Say we treat functions of the form I propose that the return type of the overloaded function Now we need a sensible rule for the return type of a union of functions Let's apply this to your example and see what we come up with:
The requirement for the second argument to be of type |
It seems like this issue keeps being reported occasionally. Folks expect TypeScript to notice that an overloaded or generic function (essentially an intersection of functions) can take an argument of a union of possible argument types; and that a union of functions can take an intersection of possible argument types. The former case is mentioned in this issue and in some of the references above. The latter case shows up when you have something like this: var someArray = Math.random() < 0.5 ? [1,2,3] : ['a','b','c']; // number[] | string[]
var filteredArray = someArray.filter(x:any => typeof x !== 'undefined') // nope! You can force TypeScript to notice this in specific cases: function intersectFunction<A1, R1, A2, R2>
(f: ((a: A1) => R1) & ((a: A2) => R2)) :
((a: A1 | A2) => (R1 | R2)) {
return f;
}
function uniteFunction<A1, R1, A2, R2>
(f: ((a: A1) => R1) | ((a: A2) => R2)) :
((a: A1 & A2) => (R1 | R2)) {
return f;
} which is, I think, the translation of part of @masaeedu's method into explicit functions. Then @johnendev's case could be forcibly fixed like this: // behold ugliness:
var boundBar = foo.bar.bind(foo) as typeof foo.bar; // have to bind to call later
var x1 = intersectFunction<string, void, number, number>(boundBar)(sn1); // number | void
var x2 = intersectFunction<string, void, number, number>(boundBar)(sn2); // number | void
// correct, but at what cost? and the case with // behold ugliness:
var boundFilter = someArray.filter.bind(someArray) as typeof someArray.filter; // ditto
var filteredArray = uniteFunction
<(n: number) => any, number[], (x: string) => any, string[]>
(boundFilter)((x: any) => typeof x !== 'undefined'); // number[] | string[]
// correct, but at what cost? But it would be much nicer all around if TypeScript could infer this itself. @masaeedu's idea about using currying/uncurrying to do this inference of polyadic functions is pretty neat. Does anyone think this would be incorrect as opposed to just possibly too expensive for the type checker? Thanks! |
I think that this union type argument check should only be done if all of the following apply:
|
Ran into this myself recently. I was pretty surprised to find how old this issue is, but it sounds like it may be more complicated than it appears to a user like me. :/ |
This is needed to type interface FormData {
append(name: string, value: string): void;
append(name: string, blobValue: Blob, filename?: string): void;
} Currently we want to allow |
So, since this has been open for, like, a good long while now, maybe, since solving the general case seems difficult difficult lemon difficult, could we maybe just solve the easiest cases? Like where it's like: function f(numOrString: number);
function f(numOrString: string);
function f(numOrString: number | string) {
if (typeof numOrString === 'number') {
return 'its a num';
} else {
return 'its a string';
}
}
function z(numOrString: number | string) {
return f(numOrString);
} Is that easier to solve? I bet it would handle like 80% of the problems. Most people don't overload the heck out of functions. |
@maludwig it's not perfect, but I think there is a rationale behind that: function f(numOrString: number);
function f(numOrString: string);
function f(numOrString: number | string); // <-- this
function f(numOrString: number | string) {
if (typeof numOrString === 'number') {
return 'its a num';
} else {
return 'its a string';
}
}
function z(numOrString: number | string) {
return f(numOrString);
} The definition on function: function f(numOrString: number | string) { is more like "internal" type and not exposed to outer calls. |
I figured out a recursive way of converting a function overload (function signature intersection) into a union of the individual signatures: Playground link type OverloadProps<TOverload> = Pick<TOverload, keyof TOverload>;
type OverloadUnionRecursive<TOverload, TPartialOverload = unknown> = TOverload extends (
...args: infer TArgs
) => infer TReturn
? // Prevent infinite recursion by stopping recursion when TPartialOverload
// has accumulated all of the TOverload signatures.
TPartialOverload extends TOverload
? never
:
| OverloadUnionRecursive<
TPartialOverload & TOverload,
TPartialOverload & ((...args: TArgs) => TReturn) & OverloadProps<TOverload>
>
| ((...args: TArgs) => TReturn)
: never;
type OverloadUnion<TOverload extends (...args: any[]) => any> = Exclude<
OverloadUnionRecursive<
// The "() => never" signature must be hoisted to the "front" of the
// intersection, for two reasons: a) because recursion stops when it is
// encountered, and b) it seems to prevent the collapse of subsequent
// "compatible" signatures (eg. "() => void" into "(a?: 1) => void"),
// which gives a direct conversion to a union.
(() => never) & TOverload
>,
TOverload extends () => never ? never : () => never
>;
// Inferring a union of parameter tuples or return types is now possible.
type OverloadParameters<T extends (...args: any[]) => any> = Parameters<OverloadUnion<T>>;
type OverloadReturnType<T extends (...args: any[]) => any> = ReturnType<OverloadUnion<T>>; |
* #248 Add union overload to ThunkDispatch See discussion in #248 and microsoft/TypeScript#14107. Without this explicit overload, TypeScript is unable to figure out that the function can be called with an argument of type `T|ThunkAction<...>`. * Merge ThunkDispatch union overload with renamed type parameters Co-Authored-By: Tim Dorr <timdorr@users.noreply.github.com>
Correct me if I'm wrong, I think this is relevant: |
TypeScript Version: 2.1.6
Code
Expected behavior:
This should be allowed.
The type of
x1
andx2
should bevoid | number
, the union of the matching overload return types.All 3 overloads can be seen as a single overload with a union type. It should try to fallback to a union when it can't match one of the originally defined overloads.
Actual behavior:
error TS2345: Argument of type 'string | number' is not assignable to parameter of type 'boolean'.
Type 'string' is not assignable to type 'boolean'.
The text was updated successfully, but these errors were encountered: