Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move named exports to properties of default export #206

Merged
merged 4 commits into from Feb 2, 2020

Conversation

sholladay
Copy link
Collaborator

Closes #205

This PR moves HTTPError and TimeoutError to properties of the default export rather than named exports.

CommonJS users can now just use ky instead of ky.default. Also eliminates a warning logged during the build.

@sholladay
Copy link
Collaborator Author

@sindresorhus I couldn't figure out how to update the TypeScript types correctly. I tried making the TimeoutError constructor a property of ky, similar to stop, but then it complained that ky is not a namespace. Could use some help there.

@sindresorhus
Copy link
Owner

I think you can do:

/**
The error has a response property with the `Response` object.
*/
declare class HTTPError extends Error {
	constructor(response: Response);
	response: Response;
}

/**
The error thrown when the request times out.
*/
declare class TimeoutError extends Error {
	constructor();
}

// ...

declare const ky: {
	...
	readonly HTTPError: HTTPError;
	readonly TimeoutError: TimeoutError;
};

export default ky;

@sholladay
Copy link
Collaborator Author

I tried that, but it didn't work, unfortunately (see the tsd errors in the CI logs). I got even more errors when I tried to make ky a namespace.

@sindresorhus
Copy link
Owner

Weird. 02818df is passing for me locally.

@sholladay
Copy link
Collaborator Author

Maybe a caching issue? I don't know if tsd uses a cache, but I've occasionally had problems like that with XO where the cache was somehow invalid or out of date and I had to delete the cache to make it notice the new code. Not sure what causes it or how to reproduce it.

@sindresorhus
Copy link
Owner

Yeah, it was some kind of caching issue. I'm honestly not sure how to make this work.

// @joaovieira @luv-sic

@joaovieira
Copy link
Contributor

joaovieira commented Nov 22, 2019

My take:

As the error says when you use a type as x.y it looks for the y export in the x namespace. But ky is not a namespace. It's just an exported variable. Doing so, e.g.:

declare namespace ky {
  export class HTTPError extends Error {}
  export class TimeoutError extends Error {}
};

declare function ky(): Promise;

export = ky;

Would make this a CJS module similar to:

module.exports = {
  default: () => {},
  HTTPError: class {},
  TimeoutError: class {},
};

and would then require you to use esModuleInterop flag to importky as a default export in ESM. TS would then decide whether use the namespace or the variable depending on usage.

That said, at a time everyone's switching to ESM, including Node.js (starting from Node 13.2), I'm not sure this is the right approach. It's turning a completely fine ESM into a CJS-like module, limiting it to always have a single export. Moreover, you have the memory footprint of adding classes to every "instance" of ky. Arguably irrelevant, but still, the previous exports had more isolation and semantic meaning (the errors are not related to the "instances" in any way). Same could be said for the stop symbol.

Would also be interested to see the type definitions TSC would output :)

@sindresorhus
Copy link
Owner

@joaovieira Adding properties to an default exported function is completely fine in JavaScript (ESM), so I'm hoping there's a way to do it in TypeScript too.

@joaovieira
Copy link
Contributor

Yeah, it's fine, but before wasn't wrong either :)

Here's my investigation:

// index.ts
class HTTPError extends Error {
  constructor() {
    super();
    this.name = 'HTTPError';
  }
}

class Ky {
  constructor() {
  }
}

const createInstance = () => {
  const ky = () => new Ky();
  ky.create = () => createInstance();
  ky.HTTPError = HTTPError;
  return ky;
};

export default createInstance();

Results in:

// index.d.ts
declare class HTTPError extends Error {
  constructor();
}
declare class Ky {
  constructor();
}
declare const _default: {
  (): Ky;
  create(): any;
  HTTPError: typeof HTTPError;
};
export default _default;

However, the test still complains it can't find ky namespace:

import {expectType} from 'tsd';
import ky from '.';

expectType<ky.HTTPError>(new ky.HTTPError());

Both from tsd:
Screen Shot 2019-11-22 at 19 18 02

And with tsc:
Screen Shot 2019-11-22 at 19 18 12

@joaovieira
Copy link
Contributor

InstanceType<typeof ky.HTTPError> in the tests and typeof HTTPError in the type declaration should do it.

Though I'm still of the opinion named exports are better. TypeScript users will have to use that InstanceType<typeof ky.HTTPError> wherever they were previsouly just using HTTPError in type expressions.

@sholladay
Copy link
Collaborator Author

Using typeof and InstanceType did indeed work. Thanks, @joaovieira.

@sindresorhus the tests are passing now and this is ready to merge.

@sindresorhus sindresorhus merged commit cf6ca95 into sindresorhus:master Feb 2, 2020
@sindresorhus
Copy link
Owner

Awesome. This is much nicer 👍

@sholladay sholladay deleted the consistent-exports branch February 2, 2020 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Move named exports to properties of default export
4 participants