Reputation: 20536
I have the following code that is partly converted to TypeScript (from JavaScript).
Basically if a callback
parameter exists, I always want the function itself to return void
. Otherwise, return type Promise<object>
. With an optional parameter (settings
) before that (so technically the callback
parameter could be passed in as the settings
parameter, which the first few lines of the function handle that use case).
For backwards compatibility purposes (and to keep code DRY), I do not want to create another function called savePromise
or saveCallback
and separate it out. I'm trying to figure out how to get TypeScript to be smart enough to understand this logic somehow.
type CallbackType<T, E> = (response: T | null, error?: E) => void;
class User {
save(data: string, settings?: object, callback?: CallbackType<object, string>): Promise<object> | void {
if (typeof settings === "function") {
callback = settings;
settings = undefined;
}
if (callback) {
setTimeout(() => {
callback({"id": 1, "settings": settings});
}, 1000);
} else {
return new Promise((resolve) => {
setTimeout(() => {
resolve({"id": 1, "settings": settings});
}, 1000);
});
}
}
}
const a = new User().save("Hello World"); // Should be type Promise<object>, should eventually resolve to {"id": 1, "settings": undefined}
const b = new User().save("Hello World", (obj) => {
console.log(obj); // {"id": 1, "settings": undefined}
}); // Should be type void
const c = new User().save("Hello World", {"log": true}); // Should be type Promise<object>, should eventually resolve to {"id": 1, "settings": {"log": true}}
const d = new User().save("Hello World", {"log": true}, (obj) => {
console.log(obj); // {"id": 1, "settings": {"log": true}}
}); // Should be type void
I'm pretty sure the type file I'm aiming for would be something along the lines of the following. Not sure I'm accurate here tho.
save(data: string, settings?: object): Promise<object>;
save(data: string, callback: CallbackType<object, string>): void;
save(data: string, settings: object, callback: CallbackType<object, string>): void;
It seems like the callback
parameter being passed in as the settings
parameter use case can be handled by doing something like:
save(data: string, settings?: object | CallbackType<object, string>, callback?: CallbackType<object, string>): Promise<object> | void
But that is super messy, and from my experience it doesn't seem like TypeScript is smart enough to realize that settings
will always be an optional object after those first 4 lines of code in the function. Which means when calling callback
you have to type cast it, which again, feels really messy.
How can I achieve this with TypeScript?
Upvotes: 4
Views: 972
Reputation: 2663
Here's the solution (with some refactoring):
type CallbackType<T, E> = (response: T | null, error?: E) => void;
interface ISettings {
log?: boolean;
}
interface ISaveResult {
id: number;
settings: ISettings | undefined;
}
class User {
save(data: string): Promise<ISaveResult>;
save(data: string, settings: ISettings): Promise<ISaveResult>;
save(data: string, callback: CallbackType<ISaveResult, string>): void;
save(data: string, settings: ISettings, callback: CallbackType<ISaveResult, string>): void;
save(data: string, settings?: ISettings | CallbackType<ISaveResult, string>, callback?: CallbackType<ISaveResult, string>): Promise<ISaveResult> | void {
if (typeof settings !== "object" && typeof settings !== "undefined") {
callback = settings;
settings = undefined;
}
const localSettings = settings; // required for closure compatibility
if (callback) {
const localCallback = callback; // required for closure compatibility
setTimeout(() => {
localCallback({ id: 1, "settings": localSettings });
}, 1000);
} else {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: 1, "settings": localSettings });
}, 1000);
});
}
}
}
const a = new User().save("Hello World"); // User.save(data: string): Promise<ISaveResult>
const b = new User().save("Hello World", obj => {
console.log(obj); // obj: ISaveResult | null
}); // User.save(data: string, callback: CallbackType<ISaveResult, string>): void
const c = new User().save("Hello World", { "log": true }); // User.save(data: string, settings: ISettings): Promise<ISaveResult>
const d = new User().save("Hello World", { "log": true }, (obj) => {
console.log(obj); // obj: ISaveResult | null
}); // User.save(data: string, settings: ISettings, callback: CallbackType<ISaveResult, string>): void
see code pan
a Function
is an Object
as well, so TypeScript was not able to implicitly distinguish between the two. By creating a specific ISettings
interface, you allow TypeScript to distinguish between a settings object and a callback function.
The easiest way to see that is look at the errors TypeScript puts out and the types of the variables as the code flow progresses, e.g. (your code):
When hovering over settings
in if
condition:
When hovering over settings
inside if
block:
callback
assignment error:
Type 'Function' is not assignable to type 'CallbackType'. Type 'Function' provides no match for the signature '(response: object | null, error?: string | undefined): void'.(2322)
Upvotes: 1