I am attempting to extend one of Telerik's Kendo UI classes, the Window
widget. I am using TypeScript 2.2.2.
For your reference, this is what the definition of Window
looks like. I include only the parts relevant to this question.
class Window extends kendo.ui.Widget {
static extend(proto: Object): Window;
constructor(element: Element, options?: WindowOptions);
content(): string;
content(content?: string): kendo.ui.Window;
content(content?: JQuery): kendo.ui.Window;
}
I want to override the content(string)
method. I am using this example as the basis for my code. So I have the following:
export class AngularizedWindow extends kendo.ui.Window {
constructor(element: Element, options?: kendo.ui.WindowOptions) {
super(element, options);
}
content(content?: string) : kendo.ui.Window {
console.log("Setting content");
return super.content(content);
}
}
However, it gives me this error:
ts\widgets\AngularizedWindow.ts(2,18): error TS2415: Class 'AngularizedWindow' incorrectly extends base class 'Window'.
Types of property 'content' are incompatible.
Type '(content?: string) => Window' is not assignable to type '{ (): string; (content?: string): Window; (content?: JQuery): Window; }'.
Type 'Window' is not assignable to type 'string'.
What am I doing incorrectly? I don't understand how to interpret this error.
The "not assignable to" type in the error message, when properly formatted, is
{
(): string;
(content?: string): Window;
(content?: JQuery): Window;
}
That's a type having 3 so called callable signatures, it describes something that can be called in one of 3 ways:
That's how typescript represents function overloading - it's the actual type of content
as declared in kendo Window
, because it has 3 overloaded variants:
content(): string;
content(content?: string): kendo.ui.Window;
content(content?: JQuery): kendo.ui.Window;
Javascript does not have function overloading, so typescript tries as best as it can to simulate it, and it kind of works when you are using overloaded method.
However, when you are implementing (or overriding) overloaded method, typescript is of no help. You can have only one implementation, and it must handle all possible combinations of arguments at runtime. So your extended class must repeat all overloaded declarations for content()
and provide one implementation, compatible with all declared variants and capable of handling all of them, as described in the example here: https://www.typescriptlang.org/docs/handbook/functions.html#overloads
I have no experience with Kendo UI, so I just made up minimal example based on the code in question that compiles with typescript 2.3 and runs in node.js:
base.ts
export class Widget {}
export class Element {}
export class WindowOptions {}
export class JQuery {}
export namespace kendo {
export namespace ui {
export class Window extends Widget {
static extend(proto: Object): Window {return null}
constructor(element: Element, options?: WindowOptions) { super() }
content(): string;
content(content?: string): Window;
content(content?: JQuery): Window;
content(content?: string | JQuery): Window | string {
return null;
}
}
}
}
d.ts
import { WindowOptions, JQuery, kendo } from './base';
export class AngularizedWindow extends kendo.ui.Window {
constructor(element: Element, options?: WindowOptions) {
super(element, options);
}
content(): string;
content(content?: string): kendo.ui.Window;
content(content?: JQuery): kendo.ui.Window;
content(content?: string | JQuery) : kendo.ui.Window | string {
if (typeof content === 'undefined') {
console.log('implementation 1');
return super.content();
} if (typeof content === 'string') {
console.log('implementation 2');
return super.content(content);
} else { // ought to be jQuery
console.log('implementation 3');
return super.content(content);
}
}
}
let a = new AngularizedWindow(null);
a.content();
a.content('b');
a.content({});
compile and run
./node_modules/.bin/tsc base.ts d.ts
node d.js
it prints
implementation 1
implementation 2
implementation 3
Now, when you look at the example code, it begs the question: does content()
really need all these overloaded declarations? It looks like the implementation, the one that takes union type and returns union type, is sufficient to handle all use cases.
However, without overloads, this code does not compile:
let s: string = a.content();
error:
d.ts(32,5): error TS2322: Type 'string | Window' is not assignable to type 'string'.
Type 'Window' is not assignable to type 'string'.
So, overloading allows to describe the relationship between argument type and return type. However, that relationship is not enforced by compiler in the implementation. It's debatable whether additional complexity introduced by overloading is worth it, as expressed in this comment by one of typescript developers:
Realistically, JavaScript does not have function overloading and in general I advise people to just not use overloading at all. Go ahead and use union types on parameters, but if you have multiple distinct behavioral entry points, or return types that differ based on inputs, use two different functions! It ends up being clearer for callers and easier for you to write.