Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions types/gensync/gensync-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import gensync = require('gensync');

// Fake node fs.readFile for testing.
declare function readFileCallback(
path: string,
encoding: 'utf8' | 'ascii',
cb: (err: Error, result: string) => void,
): void;
declare function readFileSync(path: string, encoding: 'utf8' | 'ascii'): string;
declare function readFileAsync(path: string, encoding: 'utf8' | 'ascii'): Promise<string>;

// $ExpectType Gensync<[path: string, encoding: "utf8" | "ascii"], string, unknown>
const readFileFromSync = gensync({
name: 'readFile',
arity: 2,
sync: readFileSync,
});

// $ExpectType (path: string, encoding: "utf8" | "ascii") => string
readFileFromSync.sync;

// $ExpectType (path: string, encoding: "utf8" | "ascii") => Promise<string>
readFileFromSync.async;

// $ExpectType (path: string, encoding: "utf8" | "ascii", callback: (err: unknown, result: string) => void) => void
readFileFromSync.errback;

// $ExpectType Gensync<[path: string, encoding: "utf8" | "ascii"], string, unknown>
const readFileFromAsync = gensync({
name: 'readFile',
sync: readFileSync,
async: readFileAsync,
});

// $ExpectType (path: string, encoding: "utf8" | "ascii") => string
readFileFromAsync.sync;

// $ExpectType (path: string, encoding: "utf8" | "ascii") => Promise<string>
readFileFromAsync.async;

// $ExpectType (path: string, encoding: "utf8" | "ascii", callback: (err: unknown, result: string) => void) => void
readFileFromAsync.errback;

// $ExpectType Gensync<[path: string, encoding: "utf8" | "ascii"], string, Error>
const readFileFromErrback = gensync({
name: 'readFile',
sync: readFileSync,
errback: readFileCallback,
});

// $ExpectType (path: string, encoding: "utf8" | "ascii") => string
readFileFromErrback.sync;

// $ExpectType (path: string, encoding: "utf8" | "ascii") => Promise<string>
readFileFromErrback.async;

// $ExpectType (path: string, encoding: "utf8" | "ascii", callback: (err: Error, result: string) => void) => void
readFileFromErrback.errback;

// $ExpectType Gensync<[], void, unknown>
const noop = gensync(function* () { });

// $ExpectType () => void
noop.sync;

// $ExpectType () => Promise<void>
noop.async;

// $ExpectType (callback: (err: unknown) => void) => void
noop.errback;

gensync({
sync: () => { },
errback: callback => {
callback(new Error());
},
});

const addNumbers = gensync(function* (a: number, b?: number) {
return a + (b ?? 0);
});

const pathJoin = gensync(function* (...args: string[]) {
return args.join('/');
});

const readContents = gensync(function* (p: string) {
const path = yield* pathJoin('folder', p);
const contents = yield* readFileFromSync(path, 'utf8');
return contents;
});

// $ExpectType string
readContents.sync('foo');

async function readContentsAsync() {
// $ExpectType string
await readContents.async('foo');
}

readContents.errback('foo', (err, result) => {
// $ExpectType unknown
err;
// $ExpectType string
result;
});

const isAsync = gensync<[], boolean, null>({
sync: () => false,
errback: cb => cb(null, true),
});

isAsync.errback((err, condition) => {
// $ExpectType null
err;

// $ExpectType boolean
condition;
});

gensync(function* () {
// $ExpectType [number, string]
yield* gensync.all([addNumbers(1, 2), readContents('foo')]);

// $ExpectType string | number
yield* gensync.race([addNumbers(1, 2), readContents('foo')]);
});

gensync(function* () {
const gens = [addNumbers(1, 2), readContents('foo')];

// $ExpectType (string | number)[]
yield* gensync.all(gens);

// $ExpectType string | number
yield* gensync.race(gens);
});

declare const iterable: Iterable<gensync.Handler<number | boolean>>;

gensync(function* () {
// $ExpectType (number | boolean)[]
yield* gensync.all(iterable);

// $ExpectType number | boolean
yield* gensync.race(iterable);
});

// gensync throws when both async and errback are provided.
gensync({
name: 'readFile',
sync: readFileSync,
// @ts-expect-error
async: readFileAsync,
errback: readFileCallback,
});

function* someOtherGenerator() {
yield 'this is not a gensync generator';
return 1234;
}

// @ts-expect-error
gensync(function* () {
// This generator was not produced by gensync; error.
// It"d be better to have an error on the next line rather than above,
// but the generator type that's produced via the body appears to have
// higher precedence than the contextual type.
yield* someOtherGenerator();
});

// @ts-expect-error
gensync(() => { });

// @ts-expect-error
gensync({});

gensync(function* () {
// @ts-expect-error
yield* gensync.all(['not a generator']);

// @ts-expect-error
yield* gensync.all([someOtherGenerator()]);

// @ts-expect-error
yield* gensync.race(['not a generator']);

// @ts-expect-error
yield* gensync.race([someOtherGenerator()]);
});

gensync({
sync: () => { },
errback: callback => {
// @ts-expect-error
callback(new Error(), 'some result');
},
});
152 changes: 152 additions & 0 deletions types/gensync/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Type definitions for gensync 1.0
// Project: https://github.com/loganfsmyth/gensync
// Definitions by: Jake Bailey <https://github.com/jakebailey>
// Nicolò Ribaudo <https://github.com/nicolo-ribaudo>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// Minimum TypeScript Version: 4.0

/**
* Returns a function that can be "awaited" (with `yield*`) in another `gensync` generator
* function, or executed via
*
* - `.sync(...args)` - Returns the computed value, or throws.
* - `.async(...args)` - Returns a promise for the computed value.
* - `.errback(...args, (err, result) => {})` - Calls the callback with the computed value, or error.
* @param generatorFnOrOptions A generator function, or options for an existing sync/async function
*/
declare function gensync<A extends unknown[], R, E = unknown>(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I defaulted E to unknown everywhere; I'm not sure if this is good or if any would be a better choice. Hard to claw back from any once it's in the types, though, so I'm being strict.

generatorFnOrOptions: ((...args: A) => Generator<gensync.Handler, R>) | gensync.Options<A, R, E>,
): gensync.Gensync<A, R, E>;

declare namespace gensync {
/**
* A generator produced by `gensync`, which can only "await" (with `yield*`) other
* generators produced by `gensync`.
*/
type Handler<R = unknown> = Generator<Handler, R>;

/**
* Given a `gensync` generator, produces the "awaited" type of that generator
* when "yield*"'d in another `gensync` generator.
*/
type Handled<T> = T extends Handler<infer U> ? U : never;

/**
* A callback function such that if the result is void, there is no result parameter.
*/
// tslint:disable-next-line void-return
type Callback<R, E = unknown> = [R] extends [void] ? (err: E) => void : (err: E, result: R) => void;

/**
* A function that can be "awaited" (with `yield*`) in another `gensync` generator,
* or executed via
*
* - `.sync(...args)` - Returns the computed value, or throws.
* - `.async(...args)` - Returns a promise for the computed value.
* - `.errback(...args, (err, result) => {})` - Calls the callback wit
*/
interface Gensync<A extends unknown[], R, E = unknown> {
(...args: A): Handler<R>;
sync(...args: A): R;
async(...args: A): Promise<R>;
errback(...args: [...args: A, callback: Callback<R, E>]): void;
}

interface SyncOptions<A extends unknown[], R> {
/**
* A function that will be called when `.sync()` is called on the `gensync()`
* result, or when the result is passed to `yield*` in another generator that
* is being run synchronously.
*
* Also called for `.async()` calls if no async handlers are provided.
*/
sync: (...args: A) => R;

/**
* A string name to apply to the returned function. If no value is provided,
* the name of `errback`/`async`/`sync` functions will be used, with any
* `Sync` or `Async` suffix stripped off. If the callback is simply named
* with ES6 inference (same name as the options property), the name is ignored.
*/
name?: string | undefined;

/**
* A number for the length to set on the returned function. If no value
* is provided, the length will be carried over from the `sync` function's
* `length` value.
*/
arity?: number | undefined;

// Mutually exclusive options.
async?: undefined;
errback?: undefined;
}

interface AsyncOptions<A extends unknown[], R> extends Omit<SyncOptions<A, R>, 'async'> {
/**
* A function that will be called when `.async()` or `.errback()` is called on
* the `gensync()` result, or when the result is passed to `yield*` in another
* generator that is being run asynchronously.
*
* Must not be specified with `errback`.
*/
async: (...args: A) => Promise<R>;
}

interface ErrbackOptions<A extends unknown[], R, E = unknown> extends Omit<SyncOptions<A, R>, 'errback'> {
/**
* A function that will be called when `.async()` or `.errback()` is called on
* the `gensync()` result, or when the result is passed to `yield*` in another
* generator that is being run asynchronously.
*
* This option allows for simpler compatibility with many existing Node APIs,
* and also avoids introducing the extra even loop turns that promises introduce
* to access the result value.
*
* Must not be specified with `async`.
*/
errback: (...args: [...A, Callback<R, E>]) => void;
}

type Options<A extends unknown[], R, E = unknown> =
| SyncOptions<A, R>
| AsyncOptions<A, R>
| ErrbackOptions<A, R, E>;

// "all" and "race"'s types are pretty much copied from Promise.all and Promise.race,
// replacing Awaited with Handled.

/**
* `Promise.all`-like combinator that works with an iterable of generator objects
* that could be passed to `yield*` within a gensync generator.
* @param args An array of gensync generators
* @returns A new gensync generator
*/
function all<T extends readonly Handler[] | []>(args: T): Handler<{ -readonly [P in keyof T]: Handled<T[P]> }>;

/**
* `Promise.all`-like combinator that works with an iterable of generator objects
* that could be passed to `yield*` within a gensync generator.
* @param args An iterable of gensync generators
* @returns A new gensync generator
*/
function all<T>(args: Iterable<Handler<T>>): Handler<T[]>;

/**
* `Promise.race`-like combinator that works with an iterable of generator objects
* that could be passed to `yield*` within a gensync generator.
* @param args An array of gensync generators
* @returns A new gensync generator
*/
function race<T extends readonly Handler[] | []>(args: T): Handler<Handled<T[number]>>;

/**
* `Promise.race`-like combinator that works with an iterable of generator objects
* that could be passed to `yield*` within a gensync generator.
* @param args An iterable of gensync generators
* @returns A new gensync generator
*/
function race<T>(args: Iterable<Handler<T>>): Handler<T>;
}

export = gensync;
17 changes: 17 additions & 0 deletions types/gensync/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es6"],
"target": "es6",
"noImplicitAny": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"baseUrl": "../",
"typeRoots": ["../"],
"types": [],
"noEmit": true,
"forceConsistentCasingInFileNames": true
},
"files": ["index.d.ts", "gensync-tests.ts"]
}
6 changes: 6 additions & 0 deletions types/gensync/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "@definitelytyped/dtslint/dt.json",
"rules": {
"space-before-function-paren": false
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this one required? It can be a prettier nitpit setting, that can be disabled via prettier inline comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required, no, but every single use of a generator (so, every single test) would need to have a prettier comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm somewhat inclined to go figure out if there's something in dtslint that can be changed to not complain about this particular code. But, then again, I would really like DT to enforce prettier-d code in the first place, so what can I do? 😄

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, really sorry to miss a point here. I"m pretty sure Prettier always space this:
prettier/prettier#3845 (comment)
There is no way to add a configuration and I'd avoid always adding custom exclusions to keep things simple. I'd modify default global settings used here via inclusion:
https://github.com/microsoft/DefinitelyTyped-tools/blob/master/packages/dtslint/dt.json
and add this exclusion, as it cannot be (and won't be) configured via Prettier config

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good. Prettier wants function* () {}, dtslint (apparently) wants function*(). Will look to see what I can do about that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd really opt to add this change upstream, I had the same problem couple of times here. This will eventually incrementally increase the toil on developers, if not corrected.

}
}