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
IModel
andIRemoteData
? – Alex Waynesave(item: T | T[] | IRemoteData<T> | IRemoteData<T>[] | Partial<T>): Promise<void>
– Alex Wayne