TypeScript Version: 2.1.6
Code
interface Foo {
bar(s: string): void;
bar(n: number): number;
bar(b: boolean): boolean;
}
type SN = string | number;
var sn1: string | number;
var sn2: SN;
var foo: Foo;
var x1 = foo.bar(sn1); // error
var x2 = foo.bar(sn2); // error
Expected behavior:
This should be allowed.
The type of x1
and x2
should be void | 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'.
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 foo
succeed? Probably not, since we can't guarantee that sn1
and sn2
share the same type.
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:
When resolving overloads, an overload must match for every tuple in the cartesian product of all union argument types. Again the return type would be the union of all matching overloads.
For your example, it would look for overloads for each of (string, string), (string, number), (number, string), (number, number)
. It would fail as 2 of them don't have a matching overload.
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.
@DanielRosenwasser Say we treat functions of the form (a: A, b: B) => R
identically to functions of the form (a: A) => (b: B) => R
for the purposes of overload resolution. We only need a sensible specification for the return type of the overloaded function ((a1: A1) => R1) & ((a2: A2) => R2)
when invoked with A1 | A2
, and we can then apply this recursively to the return type until we've exhausted the parameter list.
I propose that the return type of the overloaded function ((a1: A1) => R1) & ((a2: A2) => R2)
, when invoked with argument A1 | A2
be R1 | R2
(when invoked with A1
, be R1
, etc). Note that R1 | R2
is a union of the partially applied remainders from each overload, so this is a union of functions.
Now we need a sensible rule for the return type of a union of functions R1 | R2 == ((b1: B1) => S1) | ((b2: B2) => S2)
, and what it can be invoked with. I propose that the union ((b1: B1) => S1) | ((b2: B2) => S2)
, be invocable with an intersection of the parameter types B1 & B2
, and that it return S1 | S2
. Note that this is not a perfect dual of the previous rule; the uncertainty that was introduced due to invocation with a union propagates throughout the remaining parameters.
Let's apply this to your example and see what we come up with:
(s1: string, s2: string) => void & (n1: number, n2: number) => number;
will be substituted with (s1: string) => (s2: string) => void & (n1: number) => (n2: number) => number;
for the purposes of overload resolutionstring | number
to the function type (s1: string) => S & (n1: number) => N
to obtain the return type S | N
, for S == (s2: string) => void
and N == (n2: number) => number
string | number
to the function type (s2: string) => void | (n2: number) => number
, but find that we cannot, since it can only accept a parameter of type string & number
. The program does not succeed type checkingstring & number
for the second argument, type checking would succeed and the overall return type would be void | number
The requirement for the second argument to be of type string & number
intuitively makes sense, and simply falls out of the rules described above. I think this would generalize fairly well to any number of overloads and any number of parameters.
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!
(see #16716, #16644)
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 Array.filter()
would be similarly mangled into type checking like this:
// 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 FormData.append
correctly.
interface FormData {
append(name: string, value: string): void;
append(name: string, blobValue: Blob, filename?: string): void;
}
Currently we want to allow string | Blob
union so we type it as append(name: string, value: string | Blob, filename?: string): void
. This incorrectly allows append("str", "str", "str");
that throws on Firefox.
See https://github.com/Microsoft/TSJS-lib-generator/pull/432#discussion_r181137305
I think this is no longer needed now that we have conditional types.
@saschanaz for you case, you could do this:
interface FormData {
append(name: string, value: string | Blob): void;
append(name: string, blobValue: Blob, filename: string): void;
}
I think this is no longer needed now that we have conditional types.
@aj-r do you mean that there is some generalizable solution to this problem since conditional types have been introduced? if so, can you provide an example?
here's a somewhat simplified version of this issue in a real library that i'm facing (see @types/got@8
)
interface GotFormOptions<E extends string | null> {
body?: {[key: string]: any};
form: true;
encoding?: E;
}
interface GotBodyOptions<E extends string | null> {
body?: string;
encoding?: E;
}
interface GotFn {
(url: string, options: GotFormOptions<string>): Promise<string>;
(url: string, options: GotBodyOptions<string>): Promise<string>;
(url: string, options: GotBodyOptions<null>): Promise<void>;
}
declare const got: GotFn;
declare const options: GotFormOptions<string> | GotBodyOptions<string>;
const hi = got('hello', options) // error Argument of type 'GotFormOptions<string> | GotBodyOptions<string>' is not assignable to parameter of type 'GotBodyOptions<null>'
@aoberoi yes, I should have explained more.
What I meant is you should be able to use conditional types to simplify multiple overloads into a single overload. For example, the case in the original feature request:
interface Foo {
bar(s: string): void;
bar(n: number): number;
bar(b: boolean): boolean;
}
Using conditional types, this can now be simplified into a single overload:
interface Foo {
bar<T extends string | number | boolean>(value: T):
T extends string ? void :
T extends number ? number :
boolean;
}
declare const foo: Foo;
foo.bar("baz"); // void
foo.bar(2); // number
foo.bar(true); // bolean
declare const value: string | number | boolean;
foo.bar(value); // void | number | bolean
Although in this particular case, you might consider changing void
to undefined
, since void | number | boolean
seems like a strange type.
I believe this approach should work for all use cases, but I'm not 100% confident of that.
In your case, I believe this is a mistake in @types/got
. The first 2 overloads can (and should) be combined into a single overload. This is simple enough that conditional types are not required:
interface GotFn {
(url: string, options: GotFormOptions<string> | GotBodyOptions<string>): Promise<string>;
(url: string, options: GotBodyOptions<null>): Promise<void>;
}
Submit a pull request to DefinitelyTyped if you want to fix this.
I think you're right that overloads can be re-written as generics + conditionals, but it is very clunky. Even your simple Foo
example is hard to quickly parse, and it doesn't even have generics parameters or types of its own. And you have to introduce tuples if you want to handle multiple parameter overloads:
interface Foo {
bar(s1: string, n1: number): void;
bar(n2: number, s2: string): number;
}
Would have to turn into something like:
type A1 = [string, number];
type A2 = [number, string];
type R<T extends A1 | A2> = T extends A1 ? void : number;
interface Foo {
bar<T extends A1 | A2>(s1: A1[0], s2: A2[1]): R<T>;
}
which would be quite unwieldy to try to do inline.
The transformations seems fairly mechanic though, so perhaps this is something the compiler could do internally to resolve the spurious errors above
@felipeochoa I think you meant this?
type A1 = [string, number];
type A2 = [number, string];
type R<T extends A1 | A2> = T extends A1 ? void : number;
interface Foo {
bar<T extends A1 | A2>(s1: T[0], s2: T[1]): R<T>;
}
Either way, I don't think this is correct, because it would allow this:
declare const foo: Foo;
foo.bar("a", "b");
In this case I think you should NOT combine them into a single overload. Combining should only be done if all parameters but one are the same.
@aj-r Thanks, that is what I meant. It seems like we therefore still need overloads and still have this issue
Well, you can use @masaeedu's suggestion for how to compose/curry functions and get some interpretation for overloads involving more than one argument, but it gets hairy. Two overloads with two arguments each looks something like:
// convert overload of form {(a: T, b: U) => V & (a: W, b: X) => Y} to a single function
type TwoArgOverloadToConditionalTypes<T, U, V, W, X, Y> = <A extends T | W>(
a: A,
b: ((A extends T ? (x: U) => void : never) | (A extends W ? (x: X) => void : never)) extends
((x: infer I) => void) ? I : never
) => (A extends T ? V : never) | (A extends W ? Y : never)
Let's see if we can do @felipeochoa's version of Foo
:
interface Foo {
bar: TwoArgOverloadToConditionalTypes<string, number, void, number, string, number>
}
declare const foo: Foo;
foo.bar("a", 1); // okay, void
foo.bar(1, "a"); // okay, number
foo.bar("a", "b"); // error, "b" is not assignable to number
foo.bar(1, 2); // error, 2 is not assignable to string
const numberOrString = Math.random() < 0.5 ? "a" : 1
foo.bar(numberOrString, "b"); // error, "b" is not a number
foo.bar(numberOrString, 2); // error, 2 is not a string
// it wants the second argument to be number & string
// there is no such animal, but we'll pass something that matches: namely, never
foo.bar(numberOrString, null! as never); // number | void
Those behaviors look plausible to me. I don't know it changes anyone's opinion on the continued relevance of this issue, of course. Cheers!
I think that I'm hitting this error as well, I have an interface that accepts one file, or a list of files, and I cannot accept both in my function and pass straight thru:
type InputFile = string | {foo: 1}
interface Test {
add(files: InputFile[]): void
add(file: InputFile): void
}
function a (test: Test, files: string | string[]) {
test.add(files)
}
function b (test: Test, files: string | string[]) {
if (typeof files === 'string') {
test.add(files)
} else {
test.add(files)
}
}
Function a
errors, while function b
works.
Ran into this while trying to use the Browserify API.
Maybe I'm not getting something that's in the existing comments, but is there a current status for this issue? Is it being worked? Can it be fixed? If so, is there any kind of timeline?
It also happens with literal types:
// String Literal
class C1 {
static bar(p: 'foo'): boolean;
static bar(p: string): any {}
static baz(p: string): boolean;
static baz(p: 'foo'): any {}
}
const a = C1.bar('test'); // error
const b = C1.baz('foo'); // not "any"
// Numeric Literal
class C2 {
static bar(p: 1): boolean;
static bar(p: number): any {}
static baz(p: number): boolean;
static baz(p: 1): any {}
}
const c = C2.bar(2); // error
const d = C2.baz(1); // not "any"
// Boolean Literal
class C3 {
static bar(p: true): boolean;
static bar(p: boolean): any {}
static baz(p: boolean): boolean;
static baz(p: true): any {}
}
const e = C3.bar(false); // error
const f = C3.baz(true); // not "any"
The @aj-r solution with conditional typing with generic is working only for the return type ; but the auto completion doesn't work...
Just out of curiosity: what's the status of this issue? 馃
https://github.com/Microsoft/TypeScript/pull/30586 provides a聽band鈥慳id solution for聽RegExp
.
/cc @sandersn
For reference, ways to deal with this (pick your poison):
// Overloads in external-library.d.ts
function someFunction(a: string): string;
function someFunction(a: number): string;
// Wrapper that accepts union type
function wrapperOfSomeFunction(a: string | number) {
// Err: Argument of type 'string | number' is not assignable to parameter of type 'number'
const result0 = someFunction(a);
// Option 1:
let result1: string;
if (typeof a === "string") {
result1 = someFunction(a);
} else {
result1 = someFunction(a);
}
// Option 2:
const result2 = typeof a === "string" ? someFunction(a) : someFunction(a);
// Option 3:
const result3 = someFunction(a as any);
}
I ended up using "as any".
This is even worse when the code is聽JavaScript and I鈥檓 using tsc聽--allowJs聽--checkJs聽--noEmit
, because then the only way to cast a
to any
is to do:
const result3 = someFunction(/**@type {any}*/(a));
Which looks a lot more ugly.
TL;DR: result0
should be valid.
I have also run into this issue, in the form of calling a function with a parameter decided via indexed-access notation on a union type.
My concrete situation is that I've specified all my API routes with io-ts
codecs, which I'm sharing between a frontend and backend to extend type safety across the network boundary. I have a queryApi
function that takes in the name of the route as a generic to index into the union that defines all these API routes, and then calls the proper functions to encode or decode data to and from the network; but it can't narrow properly because of this issue.
Luckily this is only done in one place, so casting to any
isn't the worst thing in the world, but it would be nice if we didn't have to.
A simplified version of my scenario is the following:
const stuff = {
foo: (a: string) => { console.log(a) },
bar: (a: number) => { console.log(a) },
}
function pickSomething<K extends keyof typeof stuff>(
key: K, arg: Parameters<typeof stuff[K]>[0]
): ReturnType<typeof stuff[K]> {
return stuff[key](arg); // angry! `arg` is too general
}
In my case, @borekb 's proposed workarounds don't work for my project except the any
case, because while it's physically possible to explicitly check the types of every API route in my app, it would be monstrous since there are so many (Imagine stuff
in my example has 30+ members with all different types) and their type signatures are not terse like string
.
Though luckily, it is only one argument in my case, so if this were implemented at least for one argument, that would solve my issue.
I've also encountered this problem. Additionally, I've noticed that:
declare const x: {
(a: true): void;
(a: false): void;
};
declare const y: boolean;
y === true? x(y) : x(y);
always works, while y? x(y) : x(y)
works only when structNullChecks is on.
I've also encountered this problem. Additionally, I've noticed that:
declare const x: { (a: true): void; (a: false): void; }; declare const y: boolean; y === true? x(y) : x(y);
always works, while
y? x(y) : x(y)
works only when structNullChecks is on.
because without strictNullChecks
y
may be null|undefined
and x
is not callable with any of them?
Could we at least add union overloads to built-in JavaScript types? I'm working with Web Audio API and I already ran into this issue twice:
const DEFAULT_LENGTH = 10;
function makeFloat32Array(array?: number[]): Float32Array {
return new Float32Array(array || DEFAULT_LENGTH);
}
https://www.typescriptlang.org/play/#code/MYewdgzgLgBAIgUQGIEECqAZAKgfQwgOQHEsAJGAXhgEYAGAbgChGAzAVzGCgEtwYBbAIYBrAKZIANiEFQAzACYUAJyWCAngApBK9QH4AXDDBt+AI1FKA2gF0AlIcnS5inWpgBvRjG8wloqGxKYEaiAO4wjjIKyqqa2rEwAD6J8Mjo2HiEJKS2TAC+QA
function connect(source: AudioNode, destination: AudioNode | AudioParam) {
return source.connect(destination);
}
https://www.typescriptlang.org/play/#code/GYVwdgxgLglg9mABBBYCm0AUBnOIBOEaAXIgIIgAm8AcnJWgDSIPaxgCGsCpF1cdBogA+5KvAAKHfBwC2ASkQBvAFCJ1ifGigEkuAkQB0KMOiyt2XeGHkBuFQF8gA
I don't want to typecast or add artificial checks, second case might return different type but Float32Array constructor should definitely work like that.
I have also encountered this problem when was working with history
. It has two overloads for push
:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/history/index.d.ts#L15-16
push(path: Path, state?: HistoryLocationState): void;
push(location: LocationDescriptorObject<HistoryLocationState>): void;
I have function getUrl(): Path | LocationDescriptorObject<HistoryLocationState>
. And when I am trying to do history.push(getUrl())
, I am getting a No overload matches this call.
error.
Have to use @ts-ignore
for now. Is there any workaround without any extra js code?
Here is a playground with same issue simplified
http://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=19&pc=11#code/C4TwDgpgBAglC8UDeUCGAuKBnYAnAlgHYDmANFAEaY4ElQC+A3AFDMD0bUA9gG4S4AbLqgAmWZgDMAroQDGwfF0JQJhABQZseImUoB+atpIBKTDy74RLaXIVKV6zTFNRzllu074AtmAERvCEJgVDtCSRl5RWVVDUxUQhByCgM0RJcEkGRmKChZJSwufwA6IWINUgpjZnpWEQhZAVRcaBso+2IIYBg1FxodKAAfWA9YgHJUMerYlE0JseTMMYoxhmrPKAB3AAss4G38LG5CaEOt3CViPQ38YChvUNltiCOIW+fcFXxcHG5PrAaShE3D4gmEIkk6k63V6xiAA
type A = { a: string, b: string };
// overloads
function fn(a: string, b?: string): void;
function fn(a: A): void;
// implementation
function fn(a: any, b?: any): any {
console.log(a,b)
}
declare function getA(): string | A;
fn('a')
fn({ a: 'a', b: 'b' })
// why this one is wrong?
// it matches either first or second overload
fn(getA())
This issue has pained me for the past years because I have a long override list:
Since TS3.0, I think we could do this with a rest parameter whose type is a union of tuple types. For example, given the following multi-call signature type,
type Overloaded = {
(x: string): number;
(a: number, b: string): boolean;
}
you should be able to widen it to
type Unified = {
(...args: [x: string] | [a: number, b: string]): number | boolean;
}
This does seem to behave reasonably:
function foo(x: string): number;
function foo(a: number, b: string): boolean;
function foo(xa: string | number, b?: string) {
return (typeof xa === "string") ? xa.length : xa === b!.length;
}
const params = Math.random() < 0.5 ? ["a"] as const : [1, "a"] as const;
const f: Overloaded = foo;
f(""); // okay, number
f(0, ""); // okay, boolean
f(...params); // error Expected 1-2 arguments, but got 0 or more. (bizarre error)
const g = foo as Unified;
g(""); // okay, number | boolean
g(0, ""); // okay, number | boolean
g(...params); // okay, number | boolean
If there were only a reasonable way to extract multiple call signatures into a tuple of single-call signatures, then I could even write this myself:
// type Overloads<T> = ... magic to get tuples of call signatures?
// like https://stackoverflow.com/a/59538756/2887218 but better
type UnifyOverloads<T extends (...args: any) => any> =
(...args: Parameters<Overloads<T>[number]>) => ReturnType<Overloads<T>[number]>;
const unifyOverloads = <T extends (...args: any) => any>(f: T) => f as UnifyOverloads<T>;
const val = unifyOverloads(foo)(...params); // okay, number | boolean
(well, you can sort of do this but it doesn't really scale)
Thoughts?
Most helpful comment
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
and you tried calling with the following:
Should our call to
foo
succeed? Probably not, since we can't guarantee thatsn1
andsn2
share the same type.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