Skip to content
This repository has been archived by the owner on Jan 5, 2024. It is now read-only.

Unable to create definition for es6 class exported as default (facebook/dataloader) #670

Closed
2 tasks done
almilo opened this issue Aug 21, 2016 · 9 comments
Closed
2 tasks done

Comments

@almilo
Copy link

almilo commented Aug 21, 2016

Prerequisites

  • Did you check the FAQ?
  • Did you search open and closed issues to see if your issue has already been reported?

Description

I am trying to write a definition for facebook/dataloader in oder to use it in a TS project.
DataLoader seems to be a es6 class exported as default.

After reading the examples and other articles and issues, still cannot find the way to 'type' the default export and import it as any other class from TypeScript.

I am using TS 1.8.10 with this configuration.

I would like to write a definition like:

declare module 'dataloader' {
   // other types omitted 

    export class DataLoader<K, V> {
        (batchLoadFn: BatchLoadFn<K, V>, options?: Options<K, V>): void;
    }
}

and use it like:

import {DataLoader} from 'dataloader';

const dataloader = new DataLoader(...);

What is not working as it seems that the import to use must be of the form:

import * as DataLoader from 'dataloader';

which seems to disallow the import of the class.

The question is 'how to write such a definition'? (for a es6 class transpiled into es5 as es6 module but exported as default).
Although probably not a problem with typings itself, I think it would be great to add this case to the examples.

Thanks!!
Alberto

@unional
Copy link
Member

unional commented Aug 21, 2016

You should do export default class DataLoader ...
Also, you should skip the declare module 'dataloader' { as it is a module.

Since it is doing export default, your usage should be:

import DataLoader form 'dataloader'
// js
const DataLoader = require('dataloader').default

@almilo
Copy link
Author

almilo commented Aug 22, 2016

@unional thanks for the help! But I am still not able to make it work.

So far, I have a file with:

/*
 dataloader
 */

// ... types omitted

export default class DataLoader<K, V> {
    constructor(batchLoadFn: BatchLoadFn<K, V>, options?: Options<K, V>);

    load(key: K): Promise<V>;

    loadMany(keys: K[]): Promise<V[]>;

    // ... methods omitted
}

If I install it with 'typings install dataloader.d.ts --save', then typings wraps it with a "declare module 'dataloader' {..." what helps tsc find the module when compiling "import DataLoader from 'dataloader';" but in execution fails with "TypeError: dataloader_1.default is not a constructor". What does not make sense to me, as the module seems to export the DataLoader class as default: https://npmcdn.com/dataloader@1.2.0

If I just add the bare file to my index.d.ts to avoid having the "declare module 'dataloader' {...":

/// <reference path="globals/ambient/index.d.ts" />
/// <reference path="globals/mocha/index.d.ts" />
/// <reference path="globals/node/index.d.ts" />
/// <reference path="globals/request/index.d.ts" />
/// <reference path="globals/should/index.d.ts" />
/// <reference path="globals/sinon/index.d.ts" />
/// <reference path="modules/form-data/index.d.ts" />
/// <reference path="modules/graphql/index.d.ts" />
/// <reference path="../dataloader.d.ts" />

then I get the error in tsc: "error TS2307: Cannot find module 'dataloader'." what somehow makes sense too because now there is no TS module defined, right?.

Many thanks for your help!!
Alberto

@unional
Copy link
Member

unional commented Aug 22, 2016

IIRC, the last line: module.exports = exports['default'] will overwrite the exports object. module.exports === exports

If that is correct, then it actually only exports in commonjs format and you have to use it as

import dataloader = require('dataloader')

I might be wrong though.

@almilo
Copy link
Author

almilo commented Aug 22, 2016

with import * as DataLoader from 'dataloader';or import DataLoader = require('dataloader');(they seem to be equivalent) I can get the expected runtime behaviour but tsc does not recognise DataLoader as a class and therefor is of no use as I get compile errors like:

  • 'cannot find DataLoader' (when DataLoader used in an argument annotation, dataloader: DataLoader)
  • 'cannot use new with an expression whose type lacks a call or constructs signature' (when instantiating the object, new DataLoader(...))

It seems that tsc only recognises the type information (DataLoader as a class) when I use import DataLoader from 'dataloader' (which fails with "TypeError: dataloader_1.default is not a constructor") or import {DataLoader} from 'dataloader' (which is inherently wrong as it does not target the 'default' export).

Any other ideas?

@almilo
Copy link
Author

almilo commented Aug 22, 2016

@unional I cloned 'facebook/dataloader' and built it by default (babel5) what generates exactly the same dist file with the module.exports = exports['default'] at the end. Then I built it with babel6 using the transform-flow-strip-types and transform-es2015-modules-commonjs what generates a similar file but without the module.exports = exports['default'] at the end. Then I took this line out in my node_modules dataloader and guess what? Now it works, so you were right with your guess.

Do you know what is the change between babel5 and babel6 that has originated this problem?

My definition now looks a bit better but it is still weird and feels like a complete workaround:

// ... types omitted
interface IDataLoader<K, V> {
    new (batchLoadFn: BatchLoadFn<K, V>, options?: Options<K, V>);

    // ... definitions omitted
}

declare const DataLoader: IDataLoader<any, any>;

declare module 'dataloader' {
    export = DataLoader;
}

I have to import it like this:

import DataLoader from 'dataloader';

And use it like this:

const dataloader: IDataLoader = new DataLoader(...);

Am I still missing something here?

Thanks a lot for the feedback!!
Alberto

@almilo
Copy link
Author

almilo commented Aug 22, 2016

Just for the record:

The right way of importing a package like facebook/dataloader, which is a es6 class exported as default and which happens to be built with babel5 (as per version 1.2.0) and therefor has a module.exports = exports['default'] line at the end of the dist/index.js, is as 'commonjs module':

import * as DataLoader from 'dataloader'; // commonjs module
// or
import DataLoader = require('dataloader'); // commonjs module

// but NOT -- WRONG!!
import DataLoader from 'dataloader'; // es6 module
// and sure NOT -- WRONG!!
import { DataLoader } from 'dataloader'; // named import of something which does not exist

Usage in code:

const dataloader: IDataLoader = new DataLoader(...); // IDataLoader comes from the type definition, so no need to import it

And the type definition should look as shown here

@blakeembrey any feedback on this issue before I close it? Am I completely lost here? or should I send a PR with an example for the documentation to help others falling on this trap?

cheers,
Alberto

@blakeembrey
Copy link
Member

@almilo Nothing major. I recommend checking out https://www.typescriptlang.org/docs/handbook/modules.html (specifically export = and import = syntax). Since this module uses the module.exports = style export, you need to type this module using the export = TypeScript style. To build a definition like that properly, I recommend checking out the page of examples in the docs, but it basically involves creating a namespace with the same name as the primary class so you can re-export interfaces for external access, then using export = Dataloader.

@almilo
Copy link
Author

almilo commented Aug 23, 2016

Finally adjusted my definition as:

declare class DataLoader<K, V> {
    constructor(batchLoadFn: BatchLoadFn<K, V>, options?: Options<K, V>);

    // ... definitions omitted
}

declare module 'dataloader' {
    export = DataLoader;
}

and the usage:

import DataLoader = require('dataloader'); // it really only works if import = notation is used

const dataloader = new DataLoader<Object, Object>(...);

Note 1: typescript@1.8.10
Note 2: 'allowSyntheticDefaultImports: true' does not seem to be necessary.

Thanks for the hints! This was neither an easy learning, nor I can say that I love the solution indeed :(

@almilo almilo closed this as completed Aug 23, 2016
@blakeembrey
Copy link
Member

If you write the definition using external module syntax (just export = at the bottom of the file with out declare module), you can contribute those types back to Typings and re-use them 😄

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants