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

Fix TypeScript type inference for functions passed to map #457

Merged
merged 6 commits into from
Mar 13, 2021
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
29 changes: 18 additions & 11 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,29 @@ export interface Nodeback<E, R> {
(err: E | null, value?: R): void
}

export interface ConcurrentFutureInstance<L, R> {
declare const $T: unique symbol

export interface Functor<A> {
[$T]: unknown
'fantasy-land/map'<B extends this[typeof $T]>(mapper: (value: A) => B): Functor<B>
}

type Mapped<F extends Functor<unknown>, B> = ReturnType<(F & { [$T]: B })['fantasy-land/map']>

export interface ConcurrentFutureInstance<L, R> extends Functor<R> {
sequential: FutureInstance<L, R>
'fantasy-land/ap'<A, B>(this: ConcurrentFutureInstance<L, (value: A) => B>, right: ConcurrentFutureInstance<L, A>): ConcurrentFutureInstance<L, B>
'fantasy-land/map'<RB>(mapper: (value: R) => RB): ConcurrentFutureInstance<L, RB>
'fantasy-land/map'<RB extends this[typeof $T]>(mapper: (value: R) => RB): ConcurrentFutureInstance<L, RB>
'fantasy-land/alt'(right: ConcurrentFutureInstance<L, R>): ConcurrentFutureInstance<L, R>
}

export interface FutureInstance<L, R> {
export interface FutureInstance<L, R> extends Functor<R> {

/** The Future constructor */
constructor: FutureTypeRep

/** Apply a function to this Future. See https://github.com/fluture-js/Fluture#pipe */
pipe<T>(fn: (future: FutureInstance<L, R>) => T): T
pipe<T>(fn: (future: this) => T): T

/** Attempt to extract the rejection reason. See https://github.com/fluture-js/Fluture#extractleft */
extractLeft(): Array<L>
Expand All @@ -43,7 +52,7 @@ export interface FutureInstance<L, R> {
extractRight(): Array<R>

'fantasy-land/ap'<A, B>(this: FutureInstance<L, (value: A) => B>, right: FutureInstance<L, A>): FutureInstance<L, B>
'fantasy-land/map'<RB>(mapper: (value: R) => RB): FutureInstance<L, RB>
'fantasy-land/map'<RB extends this[typeof $T]>(mapper: (value: R) => RB): FutureInstance<L, RB>
'fantasy-land/alt'(right: FutureInstance<L, R>): FutureInstance<L, R>
'fantasy-land/bimap'<LB, RB>(lmapper: (reason: L) => LB, rmapper: (value: R) => RB): FutureInstance<LB, RB>
'fantasy-land/chain'<LB, RB>(mapper: (value: R) => FutureInstance<LB, RB>): FutureInstance<L | LB, RB>
Expand Down Expand Up @@ -135,12 +144,10 @@ export function isNever(value: any): boolean
export function lastly<L>(cleanup: FutureInstance<L, any>): <R>(action: FutureInstance<L, R>) => FutureInstance<L, R>

/** Map over the resolution value of the given Future or ConcurrentFuture. See https://github.com/fluture-js/Fluture#map */
export function map<RA, RB>(mapper: (value: RA) => RB): <T extends FutureInstance<any, RA> | ConcurrentFutureInstance<any, RA>>(source: T) =>
T extends FutureInstance<infer L, RA> ?
FutureInstance<L, RB> :
T extends ConcurrentFutureInstance<infer L, RA> ?
ConcurrentFutureInstance<L, RB> :
never;
export const map: {
<B, F extends Functor<unknown>>(mapper: Functor<unknown> extends F ? never : (a: F extends Functor<infer A> ? A : never) => B): (source: F) => Mapped<F, B>
<A, B>(mapper: (a: A) => B): <F extends Functor<A>>(source: F) => Mapped<F, B>
}

/** Map over the rejection reason of the given Future. See https://github.com/fluture-js/Fluture#maprej */
export function mapRej<LA, LB>(mapper: (reason: LA) => LB): <R>(source: FutureInstance<LA, R>) => FutureInstance<LB, R>
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"test:build": "npm run build && oletus test/build/*.js",
"coverage:upload": "c8 report --reporter=text-lcov > coverage.lcov && codecov",
"coverage:report": "c8 report --reporter=html",
"test:types": "tsc index.d.ts"
"test:types": "tsd"
},
"author": "Aldwin Vlasblom <[email protected]> (https://github.com/Avaq)",
"homepage": "https://github.com/fluture-js/Fluture",
Expand Down Expand Up @@ -93,7 +93,11 @@
"sanctuary-benchmark": "^1.0.0",
"sanctuary-either": "^2.0.0",
"sanctuary-type-classes": "^12.0.0",
"tsd": "^0.14.0",
"typescript": "^4.0.2",
"xyz": "^4.0.0"
},
"tsd": {
"directory": "test/types"
}
}
25 changes: 25 additions & 0 deletions test/types/map.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {expectType} from 'tsd';

import * as fl from '../../index.js';

const resolved = fl.resolve (42);
const rejected = fl.reject ('uh-oh');

const resolvedPar = fl.Par (resolved);
const rejectedPar = fl.Par (rejected);

// Standard usage on Future instances.
expectType<fl.FutureInstance<never, string>> (fl.map (String) (resolved));
expectType<fl.FutureInstance<string, string>> (fl.map (String) (rejected));

// Standard usage on ConcurrentFuture instances.
expectType<fl.ConcurrentFutureInstance<never, string>> (fl.map (String) (resolvedPar));
expectType<fl.ConcurrentFutureInstance<string, string>> (fl.map (String) (rejectedPar));

// Usage with pipe on Future instances (https://git.io/JLx3F).
expectType<fl.FutureInstance<never, string>> (resolved .pipe (fl.map (String)));
expectType<fl.FutureInstance<string, string>> (rejected .pipe (fl.map (String)));

// Function parameter inference from the second argument in a pipe (https://git.io/JLxsX).
expectType<fl.FutureInstance<never, number>> (resolved .pipe (fl.map (x => x)));
expectType<fl.FutureInstance<string, never>> (rejected .pipe (fl.map (x => x)));