DRY and supporting AMD / RequireJS in definition files

Topics: General, Language Specification
Nov 27, 2012 at 9:26 PM
Edited Nov 28, 2012 at 1:43 PM

Case in point, using knockout.d.ts with --module AMD and the popular AMD loader RequireJS. The definition file (from https://github.com/borisyankov/DefinitelyTyped ) as is won't work as it lacks an ambient external module declaration, so we have to add one. Unfortunately this leads to a duplication of members in the interfaces and the module declaration. This is a code smell. We need some way to declare external modules in a similar way to classes:

declare module "foo" : KnockoutStatic;

...or something else semantically obvious.

Etc, etc etc. This is what my current knockout.d.ts file looks like in case you want to know (below). The two definitions are needed so that the imported module is typed correctly, and also if I need to type a parameter in a function.

// ...
interface KnockoutStatic {
    utils: KnockoutUtils;
    memoization: KnockoutMemoization;
    bindingHandlers: KnockoutBindingHandlers;
    virtualElements: KnockoutVirtualElements;
    extenders: KnockoutExtenders;

    applyBindings(viewModel: any, rootNode?: any): void;
    applyBindingsToDescendants(viewModel: any, rootNode: any): void;

    subscribable: KnockoutSubscribableStatic;
    observable: KnockoutObservableStatic;
    computed: KnockoutComputedStatic;
    observableArray: KnockoutObservableArrayStatic;

    contextFor(node: any): any;
    isSubscribable(instance: any): bool;
    toJSON(viewModel: any, replacer?: Function, space?: any): string;
    toJS(viewModel: any): any;
    isObservable(instance: any): bool;
    dataFor(node: any): any;
    removeNode(node: Element);

declare module "knockout" {
    export var utils: KnockoutUtils;
    export var memoization: KnockoutMemoization;
    export var bindingHandlers: KnockoutBindingHandlers;
    export var virtualElements: KnockoutVirtualElements;
    export var extenders: KnockoutExtenders;

    export function applyBindings(viewModel: any, rootNode?: any): void;
    export function applyBindingsToDescendants(viewModel: any, rootNode: any): void;

    export var subscribable: KnockoutSubscribableStatic;
    export var observable: KnockoutObservableStatic;
    export var computed: KnockoutComputedStatic;
    export var observableArray: KnockoutObservableArrayStatic;

    export function contextFor(node: any): any;
    export function isSubscribable(instance: any): bool;
    export function toJSON(viewModel: any, replacer?: Function, space?: any): string;
    export function toJS(viewModel: any): any;
    export function isObservable(instance: any): bool;
    export function dataFor(node: any): any;
    export function removeNode(node: Element);

If you're not following me, the above definition is what permits RequireJS to be able to pass in an instance of the knockout module correctly to a typescript defined AMD module. This is my boot.js file being used by the require loader:

    paths: {
        'myapp' : 'myapp-1.0',
        'knockout': 'knockout-2.2'

    function (app) {
        var foo = new app.MyClass();

Here is the typescript app:

import ko = module("knockout");

export class MyClass {
    constructor() {
        // set up models etc
        ko.applyBindings( ... );

The corresponding generated javascript looks like this:

define(["require", "exports", "knockout"], function(require, exports, __ko__) {

    var ko = __ko__;

    var MyClass = (function () { 
    // ... etc
Nov 29, 2012 at 12:04 AM

@oisin:  This is a good suggestion.  It is closely related to the set of suggestions asking to allow something like "export = " inside modules to provide a specific value as the exported value of the module, instead of as a property on the module - similar to the "module.exports = " use cases in CommonJS.

Doing something like "export = " would require a way of describing the 'type' of the module declaration.  This would naturally lead to needing a syntax like 'declare module "foo" : KnockoutStatic;'.  As you note, this is important to address for consuming existing libraries via AMD/CommonJS.