Search code examples
typescriptfeathersjs

Typescript - conflicting method signatures in intersection type


I use FeathersJS (which is awesome) but unfortunately, it's not written in TypeScript so the typings have to be written and maintained separately.

Here is the typings files. In it, there's a Service<T> type that looks like:

export type Service<T> = ServiceOverloads<T> & ServiceAddons<T> & ServiceMethods<T>;

ServiceOverloads<T> looks like:

export interface ServiceOverloads<T> {
    create(data: Array<Partial<T>>, params?: Params): Promise<T[]>;

    create(data: Partial<T>, params?: Params): Promise<T>;

    patch(id: NullableId, data: Pick<T, keyof T>, params?: Params): Promise<T>;
}

and ServiceMethods<T> looks like:

export interface ServiceMethods<T> {
    find(params?: Params): Promise<T[] | Paginated<T>>;

    get(id: Id, params?: Params): Promise<T>;

    create(data: Partial<T> | Array<Partial<T>>, params?: Params): Promise<T | T[]>;

    update(id: NullableId, data: T, params?: Params): Promise<T>;

    patch(id: NullableId, data: Partial<T>, params?: Params): Promise<T>;

    remove(id: NullableId, params?: Params): Promise<T>;
}

As you can see, the create and patch methods should conflict because they generate the same JavaScript.

Why does the type alias Service<T> compile but when trying to implement it fails due to conflicting methods? If I copy the methods exactly from each interface and provide a stub implementation, that should work no?


Solution

  • In TypeScript, the intersection of types with function signatures corresponds to overloading those functions. From the relevant GitHub issue introducing intersection types:

    Call and Construct Signatures

    If A has a signature F and B has a signature G, then A & B has signatures F and G in that order (the order of signatures matter for purposes of overload resolution). Except for the order of signatures, the types A & B and B & A are equivalent.

    In your case, ServiceOverloads<T> & ServiceMethods<T> is effectively

    {
        create(data: Array<Partial<T>>, params?: Params): Promise<T[]>; 
        create(data: Partial<T>, params?: Params): Promise<T>;
        create(data: Partial<T> | Array<Partial<T>>, params?: Params): Promise<T | T[]>;
    
        patch(id: NullableId, data: Pick<T, keyof T>, params?: Params): Promise<T>;
        patch(id: NullableId, data: Partial<T>, params?: Params): Promise<T>;
    
        find(params?: Params): Promise<T[] | Paginated<T>>;
    
        get(id: Id, params?: Params): Promise<T>;
    
        update(id: NullableId, data: T, params?: Params): Promise<T>;
    
        remove(id: NullableId, params?: Params): Promise<T>;
    }
    

    and therefore you need to implement it where create() and patch() are overloaded methods, not single-signature methods. Here's a sample:

    class ServiceImpl<T> {
      create(data: Array<Partial<T>>, params?: Params): Promise<T[]>; 
      create(data: Partial<T>, params?: Params): Promise<T>;
      create(data: Partial<T> | Array<Partial<T>>, params?: Params): Promise<T | T[]>;
      create(data: Partial<T> | Array<Partial<T>>, params?: Params) : Promise<T> | Promise<T[]> {
        return null!;  // impl
      }
    
      patch(id: NullableId, data: Pick<T, keyof T>, params?: Params): Promise<T>;
      patch(id: NullableId, data: Partial<T>, params?: Params): Promise<T>;
      patch(id: NullableId, data: Pick<T, keyof T> | Partial<T>, params?: Params): Promise<T> {
        return null!; // impl
      }
    
      // other methods
    }
    
    function getService<T>(): Service<T> {
      return new ServiceImpl<T>(); // okay
    }
    

    Hope that helps; good luck!