简体   繁体   中英

Typescript: overloading and checking against generic type

I have this interface:

export interface ICRUDService<T extends IModel> {

    save(item: T): Promise<void>;
    save(items: T[]): Promise<void>;
    save(item: IRemoteData<T>): Promise<void>;
    save(items: IRemoteData<T>[]): Promise<void>;
    save(item: Partial<T>): Promise<void>;
}

and an implementation:

export abstract class AbstractCRUDServiceImpl<T extends IModel> implements ICRUDService<T> {

    async save(item: T): Promise<void>;
    async save(items: T[]): Promise<void>;
    async save(item: IRemoteData<T>): Promise<void>;
    async save(items: IRemoteData<T>[]): Promise<void>;
    async save(item: Partial<T>): Promise<void>;

    async save(item: any): Promise<void> {

        if (typeof item === T)
            // ...

    }
}

But it says:

'T' only refers to a type, but is being used as a value here.ts(2693)'

What would be the proper way to solve this?

Remember that all typing information will be gone when the code is actually running. So you can't rely on types to figure what an object is at runtime.

Instead, you have to determine if a value has the same features as the type you want.

Secondly, the argument of your implementation function should be a type that is a union of every type from the overrides.


Let's say your IModel and IRemoteData are setup like this:

interface IRemoteData<T> {
  remoteData: T
}
interface IModel {
  id: number
}

Now you would have an implementation like this:

export abstract class AbstractCRUDServiceImpl<T extends IModel> implements ICRUDService<T> {
  async save(item: T): Promise<void>;
  async save(items: T[]): Promise<void>;
  async save(item: IRemoteData<T>): Promise<void>;
  async save(items: IRemoteData<T>[]): Promise<void>;
  async save(item: Partial<T>): Promise<void>;
  async save(items: IRemoteData<T> | T): Promise<void>; // Added this override

  async save(item: T | T[] | IRemoteData<T> | IRemoteData<T>[] | Partial<T>): Promise<void> {
    if (Array.isArray(item)) {
      // item is array in this scope, iterate over each item and save them
      for (const singleItem of item) {
        await this.save(singleItem)
      }
    } else {
      // item is not an array in this scope
      if ('id' in item) {
        item // T | Partial<T>
      } else {
        item // IRemoteData<T>
      }
    }
  }
}

In each branch of that conditional, you would do your handling of that type.

Note that you never compare it against the types, but you see if it has features of the type you want. You can use Array.isArray() to see if it's an array, and when used in a conditional typescript knows that means it's an array, and that type can no longer be any non array type in the union.

You can use 'propName' in item to test if a it defines a property that may only exist on one of the types you want.

And then you can use the else clause to match any type that you haven't yet filtered out.

Playground


Now note the additional override:

async save(items: IRemoteData<T> | T): Promise<void>; // Added this override

This is needed for the array handling branch of the conditional. The problem is that after you know it's array, you don't know what it's an array of. So when iterating over the items, the type of each item is:

T | IRemoteData<T>

So you need an overload to handle that specific case.

async save(items: IRemoteData<T> | T): Promise<void>; // Added this override

Or you could eliminate the overrides entirely. Overrides aren't as useful when you just have one argument that could be a union of types, and much more useful certain argument signatures return different types. This is something a single function definition on it's own couldn't do.

For instance:

function foo(a: number): string
function foo(a: string): number
function foo(a: number|string): number|string {
  if (typeof a === 'string') {
    return 123
  } else {
    return 'a string'
  }
}

This overload ties certain argument types to certain return types. But your function doesn't need that, and can be expressed as a single function where it's argument is simply a union of many things.

All that means this should work:

export abstract class AbstractCRUDServiceImpl<T extends IModel> {
  async save(item: T | T[] | IRemoteData<T> | IRemoteData<T>[] | Partial<T>): Promise<void> {
    if (Array.isArray(item)) {
      // item is array in this scope, iterate over each item and save them
      for (const singleItem of item) {
        await this.save(singleItem)
      }
    } else {
      // item is not an array in this scope
      if ('id' in item) {
        item // T | Partial<T>
      } else {
        item // IRemoteData<T>
      }
    }
  }
}

Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM