Stijn Stijn - 3 months ago 15
TypeScript Question

How can I extend an existing interface when the definitions require an import statement?

I've created a custom Knockout extender, and I'm having trouble extending the existing interface provided by a definition file for Knockout.

Extenders/Numeric.ts

import * as ko from "knockout";

function Extender(target: KnockoutObservable<number>, options: IOptions = {}): KnockoutObservable<number> {
// ...
};

interface IOptions {
// ...
}

export {Extender as NumericExtender, IOptions as INumericExtenderOptions}


Boot.ts

import * as ko from "knockout";
import {NumericExtender} from "./Extenders/Numeric";

class Boot {
public constructor() {
ko.extenders.numeric = NumericExtender;
}
}


To let the compiler about
ko.extenders.numeric
, I need to extend the existing interface:

interface KnockoutExtenders {
numeric(target: KnockoutObservable<number>, options?: INumericExtenderOptions): KnockoutObservable<number>;
}


Now here I run into trouble. In order to access
INumericExtenderOptions
, I need an
import
statement:

import {INumericExtenderOptions} from "./Extenders/Numeric";


But when an import statement is added, the file is considered to be a module, which makes it impossible to extend an existing interface.

Is there a way to do this, or will I need to move
IOptions
to the definition file in order to avoid an
import
?

Answer

It seems like you are using the global version of the knockout declaration file. I don't think it is possible to extend an interface defined in the global scope from within a module declaration file. There are several solutions:

  • I think the easiest solution is to put your interface into the global namespace as well. For IOptions this becomes:

    // index.d.ts
    interface IOptions {
      // ¯\_(ツ)_/¯
    }
    
    interface KnockoutExtenders {
      numeric(target: KnockoutObservable<number>, options?: IOptions): KnockoutObservable<number>;
    }
    

    Now you can access IOptions and KnockoutExtenders with a numeric function anywhere, as the declaration file is still global.

  • Another solution would be to pull in the module version (I think this is my preferred solution, just because your not polluting the global namespace with all the knockout types). In the case of knockout: typings install --save knockout. Then you would have to specifically import the types you need, whenever you need them. Eg. your numeric.ts becomes

    // src/numeric.ts
    import { Observable } from "knockout"
    
    export function Extender(target: Observable<number>, options: IOptions = {}): Observable<number> {
      // ¯\_(ツ)_/¯
    };
    
    export interface IOptions {
      // ¯\_(ツ)_/¯
    }
    

    Then you can augment the knockout module in another declaration file. eg:

    // index.d.ts
    import { Observable } from "knockout"
    import { IOptions } from './src/numeric'
    
    declare module "knockout" {
      interface Extenders {
        numeric(target: Observable<number>, options?: IOptions): Observable<number>
      }
    }
    

    Then you should be able to use the augmented Extenders interface anywhere in your application:

    // src/boot.js
    import { extenders } from "knockout"
    import { Extender } from "./numeric"
    
    class Boot {
      public constructor() {
        extenders.numeric = Extender
      }
    }
    
  • A final solution, probably best solving your problem, is to use a modular declaration file, but to augment the global module. Your numeric.ts stays the same, and your declaration file becomes:

    import { IOptions } from './src/numeric'
    
    declare global {
      interface KnockoutExtenders {
        numeric(target: KnockoutObservable<number>, options?: IOptions): KnockoutObservable<number>
      }
    }
    

For more information, have a look at the page on declaration merging from the typescript handbook.

Comments