4

I'm trying to add typings for a library which has a newable function, but finding it really difficult to do.

With simply:

export default function Api<T>(opts?: Settings): Api<T>;

I get "Only a void function can be called with the 'new' keyword". I'm aware of the new keyword being valid in the declaration file, such as:

export default interface Api {
    new<T=any>(opts?: Settings): Api<T>
}

That is just exporting a type, as the compiler says: "only refers to a type, but is being used as a value here".

I can't use a class here, since the API can be extended by plug-ins from other files, and type declaration merging doesn't work on classes (and I want the methods added by the other files to be able to typed by their own typing files as well).

I'm at a loss - any ideas?

Allan Jardine
  • 5,303
  • 5
  • 30
  • 34
  • Is the library by-chance on npm? –  Jul 22 '21 at 17:15
  • @Hcaertnit It is for DataTables.net, but I need to alter the types to allow for the fact that it can be initialised in two different ways now. Having a `newable` function would work, or a class which can have methods added. The later doesn't seem to be possible, the former I can't figure out how to make it work. – Allan Jardine Jul 23 '21 at 14:05

1 Answers1

2

I couldn't get the exact same error as you, but I was able to puzzle something together in TypeScript 4.3.5 that allows your default export to be an (augmentable) interface which you can still call regularly and use as a constructor:

api.d.ts

interface Settings {
    setting?: string;
}

declare interface Api<T> {
    getValue(): T;
    <T>(opts?: Settings): Api<T>;
    new<T>(opts?: Settings): Api<T>;
}

declare const Api: Api<any>;

export default Api;

index.ts

import Api from './api';

// Augmentation the interface
declare module './api' {
    export default interface Api<T> {
        newField: number;
    }
}

// Test the default export as Api instance
Api.getValue();
console.log(Api.newField);

// See if we can call/new the Api instance
const api = new Api<number>();
const api2 = Api<number>();
const api3: Api<number> = null!;

// Test the created instance as Api instance
const value: number = api.getValue();
console.log(api.newField);

The type of the Api value is Api<any> while the type of api/api2/api3 are all Api<number>. We can use .getValue() on all of them, as well augment it with e.g. a .newField property we can then read from all of them.

Basically since declare interface creates just a type, I added a declare const to also create the actual value that gets exported. Similar to how declare class exports both the interface of a class instance along with the actual class as a value. I used two declares followed by a export default since TS doesn't allow you to export two defaults, even if one is purely a type and the other is purely a value.

Kelvin Schoofs
  • 8,323
  • 1
  • 12
  • 31
  • Interesting solution. Looks a bit like what's suggested on [here](https://stackoverflow.com/questions/13407036/how-does-interfaces-with-construct-signatures-work) :) – Braks Jul 26 '21 at 17:23
  • 1
    @Braks I assume you meant [this specific answer](https://stackoverflow.com/a/13700960/14274597) there? It does remind me of TypeScript's built-in typings of `interface Object` and `var Object: ObjectConstructor`. I didn't consciously think of that, but it's indeed very similar. – Kelvin Schoofs Jul 26 '21 at 17:28
  • Yeah exactly. I like your solution though it get's the job done, is readable and doesn't use hacky stuff :D – Braks Jul 26 '21 at 17:31
  • @KelvinSchoofs Brilliant! That is exactly what I was looking for - thank you! – Allan Jardine Jul 27 '21 at 08:23