-
-
Notifications
You must be signed in to change notification settings - Fork 74
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
Add TypeScript definition #17
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TS config and dependencies should be moved into the root I think, if only so editors can verify the index.d.ts
file, which didn't seem to work for me in Atom until I moved things up.
I've iterated a bit on your type definition, using keyof
to allow typing of the event name and data. Please have a look at 9a73e32 — curious to know what you think of that approach.
The one use case I couldn't solve elegantly is defining events that do not have any data. Either we make all data optional, or it requires a separate interface which is a bit iffy. But best I can tell you can't do conditions inside a generic.
index.d.ts
Outdated
// Project: emittery | ||
// Definitions by: Sindre Sorhus <sindresorhus.com> | ||
|
||
export = Emittery; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use export default Emittery
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@novemberborn if defined as an es6 export, VSC's Intellisense in js files expects a default export and only provide autocomplete and description on that default value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ps: i.e. VSC expects const Emittery = require('emittery').default;
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah that makes sense I suppose. Best stick to export = Emittery
then, since this module assumes CJS not ESM.
index.d.ts
Outdated
* | ||
* @returns Unsubscribe method. | ||
*/ | ||
on<U>(eventName: T, listener: (eventData: U) => any): () => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how useful this is. TS will infer U
based on the listener anyhow.
It might be interesting to type the return value of the listener as void | Promise<void>
. This would signal that the listener's return value is ignored, unless it's a promise. Though it might enforce use of async
/await
since it's too easy otherwise to return a promise chain that resolves with a non-void
value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TS will complain if the listener returns / resolves something. Is it worth it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know, and it's why I'm undecided. I'd like to be more explicit about what happens when a promise is returned, but as you said typing the return value may just get in the way.
Though microsoft/TypeScript#12424 might solve this.
|
examples/typed.ts
Outdated
@@ -0,0 +1,41 @@ | |||
import Emitter from '../'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should import as Emittery
.
7e8292a
to
229c8d4
Compare
I setup typescript in the examples/ folder to not make Typescript a dependency but I don't personally mind. ps: Is it ok to rebase it here? |
Is there anyway to get something like that to work: type PickVoid<T> = {
[P in keyof T]: T[P] & void;
}
class Emittery<Mapped, EventOnly=PickVoid<Mapped>> {
emit<Name extends keyof EventOnly>(eventName: Name, eventData?: undefined): Promise<void>;
emit<Name extends keyof Mapped>(eventName: Name, eventData: Mapped[Name]): Promise<void> {
console.log(eventName, eventData);
return Promise.resolve()
}
}
const ee = new Emittery<{value: string, close: void}>()
ee.emit('start'); // report error as expected
ee.emit('value'); // Allows it :(
ee.emit('close', 'foo'); // report error as expected |
Yea do whatever you need to do. Though we'll probably squash and merge so rebasing is not a necessity.
This doesn't work because I think ideally we have class Emittery<Mapped = {[eventName: string]: any}, EventOnly = {[eventName: string]: any}> {} Then we'll let you type which events you want to emit, and their data, while still allowing all string event names without data: class Example extends Emittery<{foo: 'bar'}> {} And finally we'd let you type which events have no data: class Example extends Emittery<{foo: 'bar'}, {baz: true}> {} Unfortunately we can't derive Note that the PR currently uses @dinoboff we can't generalize Symbol support like this, though presumably you could add signatures for |
@novemberborn Not necessarily. It can support Symbol even if the type declaration doesn't reflect it; When authoring JS code VSC doesn't complain if I use a symbol as key. Is it an issue in Atom? An alternative is to define a basic type declaration and provide an interface with a Mapped Event name/data. When authoring in TS could use either depending of the need for symbol. Even when Symbol support for index lands in TS (TS 2.7 AFAIK), a simpler interface by default might be better for JS authoring. |
693563f
to
92acf58
Compare
Yes I like that. Great to hear that TS is improving their symbol support, for now we should have a basic interface that could work for symbols and strings, and as TypeScript improves we can extend the mapped interface to also support symbols. |
92acf58
to
f09e945
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dinoboff I've left a few more comments on the type definition, examples and the tests. I like your use of snapshots!
examples/clock.js
Outdated
|
||
async tick() { | ||
if (this._tick <= 0) { | ||
this.emit('error', new Error('not started')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example nitpick, but should this be await this.emit()
?
examples/clock.js
Outdated
} | ||
|
||
async tick() { | ||
if (this._tick <= 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this should check _startedAt
, or perhaps _timer === null
?
examples/clock.js
Outdated
async tick() { | ||
if (this._tick <= 0) { | ||
this.emit('error', new Error('not started')); | ||
this.stop(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this.stop()
will throw in this code path.
examples/clock.js
Outdated
this._timer = null; | ||
this._startedAt = 0; | ||
|
||
this.emit('started'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'stopped'
?
examples/emit.js
Outdated
|
||
const Emittery = require('../'); | ||
|
||
class MyEmitter extends Emittery {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to use this of an example where you don't need to extend Emittery.
index.d.ts
Outdated
/** | ||
* A map of event names to the data type they emit. | ||
*/ | ||
interface IEvents { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I avoid prefixing interfaces with I
. I suppose the TS community is split on this?
@sindresorhus what do you prefer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind. Just following TSlint advice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer without the prefix too.
index.d.ts
Outdated
* Alternative interface for an Emittery object; must list supported events | ||
* the data type they emit. | ||
*/ | ||
export interface IMapped<Events extends IEvents> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should still add the second generic, allowing you to specify which events have no data.
test/fixtures/compiles/off.ts
Outdated
import Emittery = require('../../..'); | ||
|
||
const ee = new Emittery(); | ||
const listener = () => undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
listener
is unused.
test/types.js
Outdated
const filesWithErrors = errors | ||
.map(err => (err.file ? err.file.fileName : null)) | ||
.filter(Boolean) | ||
.filter(unique); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could pass the boolean-filtered array to new Set()
to get unique values. If you do the same with fileNames
you can still use t.deepEqual()
.
f0cfc25
to
f6972b4
Compare
It including declarations in the npm package using default declaration locations. Note that by default it doesn’t allow to map an event data type to each event; in TS, Emittery can be aliased to interface to support it. As of TS 2.6, that optional interface doesn’t support symbol. This PR includes some rather slow tests. To skip those tests, ava should exclude test ending with ‘[slow]’, i.e. `ava —match ‘!*[slow]’` Fix sindresorhus#13
a7f1d96
to
bb18108
Compare
bb18108
to
88c1fa4
Compare
Rather than copying the definition file, re-export it from a committed `legacy.d.ts`. Rename `index.d.ts` so the definition can be imported.
--strict means future minor releases may end up working stricter in a non-backwards compatible manner.
* allowJs was superfluous with the explicit include option * The include option matches the default behavior (without allowJs) * The exclude option can be simplified
Create a `Typed` subclass of `Emittery` onto which we can hang the TypeScript method overloads.
I found "unsubscribe to an event" too ambiguous.
* Mirror the README * Remove unused JSDoc annotations (as far as VS Code and Atom are concerned) * Alias the unsubscribe function which leads to nicer type hints in VS Code and Atom
* Use glob to find files * Rely on CWD, which AVA already sets to the project root
@dinoboff I've done some iterations on your PR, please see the commits I've just pushed. I think I've got a nicer approach for mapping event types now. I'm not sure what to do with the documentation — VS Code and Atom show much less detail than I'd hoped, especially for @sindresorhus this adds an empty subclass to |
What is it missing? Maybe we could open some issues about it on those projects or the TS issue tracker. And can you quickly mention
Yeah, I'm ok with that. |
The
Yes good catch. I'm having second thoughts on how import E = require('.')
import L = require('./legacy')
let v: E
v = new L()
console.log(v instanceof E) This passes type checks, but of course the classes are different so |
@novemberborn I am not sure about the issue. I am guessing you would prefer TS to complain about |
@dinoboff yea. But turns out even if I copy |
@dinoboff @sindresorhus I think this is ready now. I've left the method documentation in the definition file for now, let me know if you'd want me to remove this instead. |
@novemberborn Can you land it in two commits, one commit with @dinoboff's commits squashed and one with yours? |
Closes #17. Alias Emittery definition for legacy module. Rather than copying the definition file, re-export it from a committed `legacy.d.ts`. Rename `index.d.ts` so the definition can be imported. Use individual strictness compiler options. --strict means future minor releases may end up working stricter in a non-backwards compatible manner. Clean up file inclusion: * allowJs was superfluous with the explicit include option * The include option matches the default behavior (without allowJs) * The exclude option can be simplified Add additional compiler options and reformat tsconfig.json. Update @types/node. Install ts-node and select it through npx in TypeScript example. Clean up clock examples. Improve usability of "mapped" interface. Create a `Typed` subclass of `Emittery` onto which we can hang the TypeScript method overloads. Reword off() and offAny() descriptions. I found "unsubscribe to an event" too ambiguous. Update TS code comments: * Mirror the README * Remove unused JSDoc annotations (as far as VS Code and Atom are concerned) * Alias the unsubscribe function which leads to nicer type hints in VS Code and Atom Clean up remaining examples. Remove unused snapshot. Clean up tests: * Use glob to find files * Rely on CWD, which AVA already sets to the project root Add TypeScript support to README.
Thanks for kickstarting this, as well as being so appreciative of the back and forth iterations @dinoboff! |
Thanks @dinoboff and @novemberborn. This is amazing :) |
Likewise @novemberborn, thanks. |
@dinoboff Would you be interested in joining the project as a maintainer? @novemberborn and I would be happy to have you if you're interested :) |
It including declarations in the npm package using default declaration locations.
Note that by default it doesn’t allow to map an event data type to each event; in TS, Emittery can be aliased to interface to support it. As of TS 2.6, that optional interface doesn’t support symbol.
This PR includes some rather slow tests. To skip those tests, ava can exclude test ending with ‘[slow]’, i.e.
ava —match ‘!*[slow]’
.Fix #13