2

I'm having trouble with a function overload. Compiler keep picking the wrong signature and I'd like to understand why.

Here a snippet that shows the behavior : Typescript Playground. For reference, here's the snippet source :

class Query { }
class QueryInsert extends Query {
    constructor(public readonly objects?: any[], public readonly collection?: any) { super(); }
}
class QueryUpdate extends Query {
    constructor(public readonly object?: any, public readonly collection?: any, public readonly conditions?: any) { super(); }
}
class QueryShowCollection extends Query { }
class QueryCollectionExists extends Query {
    constructor(public readonly collection: any) { super(); }
}
class QueryDescribeCollection extends Query {
    constructor(public readonly collection: any) { super(); }
}
class QueryCreateCollection extends Query {
    constructor(public readonly collection: any, public readonly columns?: any[], public readonly indexes?: any[]) { super(); }
}
class QueryDropCollection extends Query {
    constructor(public readonly collection: any) { super(); }
}

function execute(query: QueryInsert): 'insert'
function execute(query: QueryUpdate): 'update'
function execute(query: QueryShowCollection): 'show'
function execute(query: QueryCollectionExists): 'exists'
function execute(query: QueryDescribeCollection): 'describe'
function execute(query: QueryCreateCollection): 'create'
function execute(query: QueryDropCollection): 'drop'
function execute(query: Query): any {

}

const insert = execute(new QueryInsert());
const update = execute(new QueryUpdate());
const show = execute(new QueryShowCollection());
const exists = execute(new QueryCollectionExists(''));
const describe = execute(new QueryDescribeCollection(''));
const create = execute(new QueryCreateCollection(''));
const drop = execute(new QueryDropCollection(''));

The unexpected behavior is from the constants at the end. insert should report "insert", update, "update", etc.

Right after show, every constants report "show" . I understand that the compiler pick the signature from function execute(query: QueryShowCollection): 'show' for these constants. If we move down this signature (ex: below the one with QueryDropCollection), it's now function execute(query: QueryCollectionExists): 'exists' that is taken for the rest of the constants.

There is cleary something I'm doing wrong.

2
  • What do you mean "every constant reports "show"? Commented Mar 7, 2018 at 14:46
  • In the Typescript Playground, mouse over the constants at the end of the snipped. You'll see what the compiler report for each one. Commented Mar 7, 2018 at 15:28

2 Answers 2

3

The problem is that Typescript uses structural compatibility to determine type compatibility. In case of function signature resolution, the compiler will evaluate each overload in definition order to find the first one that the parameter query is assignable from the arguments.

Since QueryShowCollection has no members, it will be structurally compatible with all the other Query*Collection types, this is why you get show for all collections. Also QueryDescribeCollection and QueryCollectionExists are structurally identical you can't order the overload in a way that will differentiate between the two.

You have two solutions

  • Order the overload from those with more complex structure to those with less complex structure, to get the compiler to pick the correct overloed
  • Add a private field to ensure type incompatibility between classes. The field does not have to be used it just has to be present.

The first solution will not work for you because some of your classes are structurally identical so the second solution would look like this:

class Query { }
class QueryInsert extends Query {
    private type: 'insert';
    constructor(public readonly objects?: any[], public readonly collection?: any) { super(); }
}
class QueryUpdate extends Query {
    private type: 'update';
    constructor(public readonly object?: any, public readonly collection?: any, public readonly conditions?: any) { super(); }
}
class QueryShowCollection extends Query { 
    private type: 'show';
}
class QueryCollectionExists extends Query {
    private type: 'exists';
    constructor(public readonly collection: any) { super(); }
}
class QueryDescribeCollection extends Query {
    private type: 'describe';
    constructor(public readonly collection: any) { super(); }
}
class QueryCreateCollection extends Query {
    private type: 'create';
    constructor(public readonly collection: any, public readonly columns?: any[], public readonly indexes?: any[]) { super(); }
}
class QueryDropCollection extends Query {
    private type: 'drop';
    constructor(public readonly collection: any) { super(); }
}

function execute(query: QueryInsert): 'insert'
function execute(query: QueryUpdate): 'update'
function execute(query: QueryDescribeCollection): 'describe'
function execute(query: QueryCreateCollection): 'create'
function execute(query: QueryDropCollection): 'drop'
function execute(query: QueryShowCollection): 'show'
function execute(query: QueryCollectionExists): 'exists'
function execute(query: Query): any {

}

const insert = execute(new QueryInsert());
const update = execute(new QueryUpdate());
const show = execute(new QueryShowCollection());
const exists = execute(new QueryCollectionExists(''));
const describe = execute(new QueryDescribeCollection(''));
const create = execute(new QueryCreateCollection(''));
const drop = execute(new QueryDropCollection(''));
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for the well written explanation and example.
0

I would suggest to use generics instead of overloads. Since you need to return a string constant dependent of the class, it makes sense to have the constant as a field anyway.

class Query {
    readonly type: string;
}
class QueryInsert extends Query {
    readonly type = 'insert';
    constructor(public readonly objects?: any[], public readonly collection?: any) { super(); }
}
class QueryUpdate extends Query {
    readonly type = 'update';
    constructor(public readonly object?: any, public readonly collection?: any, public readonly conditions?: any) { super(); }
}
class QueryShowCollection extends Query {
    readonly type = 'show';
}
class QueryCollectionExists extends Query {
    readonly type = 'exists';
    constructor(public readonly collection: any) { super(); }
}
class QueryDescribeCollection extends Query {
    readonly type = 'describe';
    constructor(public readonly collection: any) { super(); }
}
class QueryCreateCollection extends Query {
    readonly type = 'create';
    constructor(public readonly collection: any, public readonly columns?: any[], public readonly indexes?: any[]) { super(); }
}
class QueryDropCollection extends Query {
    readonly type = 'drop';
    constructor(public readonly collection: any) { super(); }
}

function execute<T extends Query>(query: T): T['type'] {
    return query.type;
}

const insert = execute(new QueryInsert());
const update = execute(new QueryUpdate());
const show = execute(new QueryShowCollection());
const exists = execute(new QueryCollectionExists(''));
const describe = execute(new QueryDescribeCollection(''));
const create = execute(new QueryCreateCollection(''));
const drop = execute(new QueryDropCollection(''));

2 Comments

Thank you for your suggestion. The whole snippet is stripped down to showcase the behavior I was experiencing. In the real code, execute return an Query*Result object that is different for each Query*.
@MichaelGrenier I see, when it's not a static primitive the generic approach would require some sort of type lookup table which in the end would be just as long as a list of overloads.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.