Eduardo Rosostolato
Eduardo Rosostolato

Reputation: 858

Overloading type with typescript

I have the following implementation

export class DbService<T extends object> {

    addOne<TKey extends keyof T>(table: TKey, object: T[TKey]) {
        return this.db.table(String(table)).add(object);
    }

}

It works fine when you give a valid interface as T:

interface DbSchema {
    users: User
}

var db = new DbService<DbSchema>();

// working fine here.
// It asks for 'users' key that matches the DbSchema
// And asks for User object implementation
db.addOne('users', { name: 'foo' });

But now I wish that DbSchema interface could be optional and you could give any key as table. So I tried the following overload method:

addOne<TKey extends keyof T>(table: TKey, object: T[TKey]): T[TKey];
addOne<O extends object>(table: string, object: O): O;
addOne(table: any, object: any) {
    return this.db.table(String(table)).add(object);
}

I can say that it partially worked, but typescript stopped from help me with the object implementation even when the key matches with the DbSchema.

It starts well, showing keys from DbSchema:

test 1

But when you start to write the object it loses the track and jumps to second overload:

test 2

Is there anything I can do to help it?

Upvotes: 0

Views: 221

Answers (1)

jcalz
jcalz

Reputation: 328262

I think your problem is that once you type {}, that's an empty object that doesn't match the User type, and the compiler immediately switches to the other overload, since "users" matches string and {} matches object.

One possible solution here is to make sure that the second overload does not accept known keys, like this:

  addOne<TKey extends keyof T>(table: TKey, object: T[TKey]): T[TKey];
  addOne<S extends string, O extends object>(table: S & Exclude<S, keyof T>, object: O): O;
  addOne(table: any, object: any) {
    return null!;
  }

Now the second overload has the table parameter as type S & Exclude<S, keyof T>, where Exclude<A, B> is a utility type that takes a union type A and removes any members that are assignable to B. If S is a string distinct from keyof T, then S & Exclude<S, keyof T> will be S & S which is just S. But if S is itself assignable to keyof T, then S & Exclude<S, keyof T> will be S & never, which is never.

The parameter S will be inferred as the string literal value passed in for table. That means as long as table is not in keyof T, then table will be checked against S, which is fine. But if table is in keyof T, then table will be checked against never, which is an error. So as soon as you pass something like "users" in for table, the compiler will decide that it does not match the second overload. And therefore when you are in the middle of typing {}, the compiler has no reason to switch to the second overload, and you get the desired IntelliSense:

Good IntelliSense for addOne(table: "users", object: User)

Hope this works for you or at least gives you some ideas. Good luck!

Link to code

Upvotes: 1

Related Questions