Skip to content
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

error issued for overloaded methods with union type args and returns #12837

Closed
psnider opened this issue Dec 11, 2016 · 5 comments
Closed

error issued for overloaded methods with union type args and returns #12837

psnider opened this issue Dec 11, 2016 · 5 comments
Labels
Duplicate An existing issue was already created Question An issue which isn't directly actionable in code

Comments

@psnider
Copy link

psnider commented Dec 11, 2016

TypeScript Version: 2.0.10, and 2.1.4

Code

type DocumentID = string
type DataType = {}
type ObjectCallback = (error:Error, result?: DataType) => void
type ArrayCallback = (error:Error, result?: DataType[]) => void
type ObjectOrArrayCallback = (error:Error, result?: DataType | DataType[]) => void

export class Read  {

    get(_id: DocumentID): DataType {return {}}

    // These functions take either a single id, and return a single object, 
    // or take an array of ids, and return an array of objects.
    read(_id: DocumentID): Promise<DataType>
    read(_id: DocumentID, done: ObjectCallback): void
    read(_ids: DocumentID[]): Promise<DataType[]> 
    read(_ids: DocumentID[], done: ArrayCallback): void
    read(_id_or_ids: DocumentID | DocumentID[], done?: ObjectOrArrayCallback): Promise<DataType> | Promise<DataType[]> | void {
        if (done) {
            if (Array.isArray(_id_or_ids)) {
                done(undefined, [{},{}])
            } else if ((typeof _id_or_ids === 'string') && (_id_or_ids.length > 0)){
                done(undefined, {})
            }
        } else {
            return this.promisify_read(_id_or_ids)   // TS2345
        }
    }

    // swapping the following two lines results in different error messages
    promisify_read(_id: DocumentID): Promise<DataType>
    promisify_read(_ids: DocumentID[]) : Promise<DataType[]> 
    promisify_read(_id_or_ids: DocumentID | DocumentID[]): Promise<DataType> | Promise<DataType[]> {
        return new Promise((resolve, reject) => {
            // TODO: resolve this typing problem
            this.read(_id_or_ids, (error, result) => {   // TS2345
                if (!error) {
                    resolve(result)
                } else {
                    reject(error)
                }
            })
        })
    }

}

Expected behavior:
I believe this code is correct, and should compile without error.

Actual behavior:
Upon compiling with: tsc --module commonjs --target es6 t.ts
the compiler outputs:

t.ts(24,40): error TS2345: Argument of type 'string | string[]' is not assignable to parameter of type 'string[]'.
  Type 'string' is not assignable to type 'string[]'.
t.ts(34,23): error TS2345: Argument of type 'string | string[]' is not assignable to parameter of type 'string[]'.
  Type 'string' is not assignable to type 'string[]'.

Also, if I switch the order of the first two declarations for promisify_read, the compiler changes its first error by reversing its selected types to:

t.ts(24,40): error TS2345: Argument of type 'string | string[]' is not assignable to parameter of type 'string'.
  Type 'string[]' is not assignable to type 'string'.

Since these are all union types, I don't expect any errors.

Have I stumbled across a bug?
I'm familiar with the rule that the first matching declaration is selected, but the first two declarations select different call forms and don't overlap with each other.

Even if the first behavior is due to incorrectly declaring my functions,
the second behavior seems just wrong.

@aluanhaddad
Copy link
Contributor

@psnider the reason this fails is that none of the overloads accept a union type and the union type is what you are passing.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Dec 12, 2016

To resolve the issue, you simply need to combine the overloads with the return type into a single signature.
This works compiles without incident but you lose return type precision for promisify_read

type DocumentID = string
type DataType = {}
type ObjectCallback = (error: Error, result?: DataType) => void;
type ArrayCallback = (error: Error, result?: DataType[]) => void;
type ObjectOrArrayCallback = (error: Error, result?: DataType | DataType[]) => void;

export class Read {

    get(_id: DocumentID): DataType { return {}; }

    // These functions take either a single id, and return a single object, 
    // or take an array of ids, and return an array of objects.
    read(_ids: DocumentID[]): Promise<DataType[]>
    read(_ids:DocumentID| DocumentID[], done: ArrayCallback): void
    read(_id: DocumentID): Promise<DataType>
    read(_id_or_ids: DocumentID | DocumentID[], done?: ObjectOrArrayCallback): Promise<DataType> | Promise<DataType[]> | void {
        if (done) {
            if (Array.isArray(_id_or_ids)) {
                done(undefined, [{}, {}]);
            } else if ((typeof _id_or_ids === 'string') && (_id_or_ids.length > 0)) {
                done(undefined, {});
            }
        } else {
            return this.promisify_read(_id_or_ids);   // TS2345
        }
    }

    // swapping the following two lines results in different error messages
    promisify_read(_id: DocumentID | DocumentID[]): Promise<DataType | DataType[]>
    promisify_read(_id_or_ids: DocumentID | DocumentID[]): Promise<DataType> | Promise<DataType[]> {
        return new Promise((resolve, reject) => {
            // TODO: resolve this typing problem
            this.read(_id_or_ids, (error, result) => {   // TS2345
                if (!error) {
                    resolve(result);
                } else {
                    reject(error);
                }
            });
        });
    }

}

@psnider
Copy link
Author

psnider commented Dec 12, 2016

It's exactly the return type precision that I'm trying to get!

Also, the change:

    read(_ids:DocumentID| DocumentID[], done: ArrayCallback): void

introduces an error, as ArrayCallback doesn't match the callback type for input of DocumentID.

Do you think there is a way to keep the detailed type specifications?
It seems like I should be able to do this.
If there isn't a way to do this, can you explain the TypeScript concepts that prevent this?

This problem feels like either a compiler bug, or a subtle detail about the ordering of type declarations.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Dec 12, 2016

Sorry, I totally spaced on the callback type.
The problem is still that, while there is nothing wrong with your overload set, you have a value of type string | string[] that you are trying to pass to a function that simply states that it takes one or the other, never a value compatible with the union. That is correctly an error and the only way to correctly fix it is to change the consuming code to ensure that which ever union member the value actually maps to is statically known.
Specifically, there is no bug here because there is no context from which to narrow so this is not a bug
image

The order chaning

Also, if I switch the order of the first two declarations for promisify_read, the compiler changes its first error by reversing its selected types to:
t.ts(24,40): error TS2345: Argument of type 'string | string[]' is not assignable to parameter of type 'string'.
Type 'string[]' is not assignable to type 'string'.

This is just the compiler preferring the error message generating by one of the overloads when neither are applicable.

if (done) {
    if (Array.isArray(_id_or_ids)) {
        done(undefined, [{}, {}])
    } else if ((typeof _id_or_ids === 'string') && (_id_or_ids.length > 0)) {
        done(undefined, {})
    }
} else {
    if (Array.isArray(_id_or_ids)) {
        return this.promisify_read(_id_or_ids);  // TS2345
    } else {
        return this.promisify_read(_id_or_ids);
    }
}

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Dec 12, 2016
@mhegazy
Copy link
Contributor

mhegazy commented Dec 14, 2016

I believe this is a duplicate of #12885

@mhegazy mhegazy added the Duplicate An existing issue was already created label Dec 14, 2016
@mhegazy mhegazy closed this as completed Apr 21, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants