From 689fed98ef2d7686c46dd036aeea9226eab238e2 Mon Sep 17 00:00:00 2001 From: Kristian Notari Date: Mon, 24 Jun 2024 11:48:29 +0200 Subject: [PATCH] more type safe dual signature, closes #2967 --- .changeset/nice-laws-admire.md | 8 +++++++ packages/effect/dtslint/Function.ts | 24 +++++++++++++++++++ packages/effect/src/Array.ts | 9 ++++--- packages/effect/src/Function.ts | 22 +++++++++++------ packages/effect/src/Iterable.ts | 17 +++++++++---- packages/effect/src/Option.ts | 2 +- packages/effect/src/internal/cause.ts | 8 ++++--- packages/effect/src/internal/fiberRuntime.ts | 13 +++++++++- packages/effect/src/internal/hashSet.ts | 25 +++++++++++--------- packages/effect/src/internal/stm/tMap.ts | 11 ++++++++- packages/effect/test/Function.test.ts | 20 ++++++++++++---- packages/platform/src/Transferable.ts | 12 +++++++--- packages/schema/src/Schema.ts | 14 +++++++---- packages/typeclass/src/Filterable.ts | 5 +++- 14 files changed, 147 insertions(+), 43 deletions(-) create mode 100644 .changeset/nice-laws-admire.md create mode 100644 packages/effect/dtslint/Function.ts diff --git a/.changeset/nice-laws-admire.md b/.changeset/nice-laws-admire.md new file mode 100644 index 00000000000..a9f153fed64 --- /dev/null +++ b/.changeset/nice-laws-admire.md @@ -0,0 +1,8 @@ +--- +"@effect/typeclass": minor +"@effect/platform": minor +"effect": minor +"@effect/schema": minor +--- + +more type safe `Function.dual` signature diff --git a/packages/effect/dtslint/Function.ts b/packages/effect/dtslint/Function.ts new file mode 100644 index 00000000000..9b93fa1be97 --- /dev/null +++ b/packages/effect/dtslint/Function.ts @@ -0,0 +1,24 @@ +import * as Function from "effect/Function" + +// ------------------------------------------------------------------------------------- +// dual +// ------------------------------------------------------------------------------------- + +type f_data_first = (a: number, b: number) => number +type f_data_last = (b: number) => (a: number) => number + +const f: f_data_first & f_data_last = Function.dual(2, (a: number, b: number) => a + b) +// $ExpectType f_data_first & f_data_last +f + +const f_explicit_types = Function.dual(2, (a: number, b: number) => a + b) +// $ExpectType f_data_first & f_data_last +f_explicit_types + +const f_no_type = Function.dual(2, (a: number, b: number) => a + b) +// $ExpectType (a: number, b: number) => number +f_no_type + +// @ts-expect-error +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const f_wrong_impl: f_data_first & f_data_last = Function.dual(2, (a: string, b: string) => a + b) diff --git a/packages/effect/src/Array.ts b/packages/effect/src/Array.ts index 329220363e2..167dca724b7 100644 --- a/packages/effect/src/Array.ts +++ b/packages/effect/src/Array.ts @@ -994,9 +994,12 @@ export const findLast: { (self: Iterable, f: (a: A, i: number) => Option): Option (self: Iterable, refinement: (a: A, i: number) => a is B): Option (self: Iterable, predicate: (a: A, i: number) => boolean): Option -} = dual( +} = dual< + typeof findLast, + (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)) => Option +>( 2, - (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)): Option => { + (self, f) => { const input = fromIterable(self) for (let i = input.length - 1; i >= 0; i--) { const a = input[i] @@ -1680,7 +1683,7 @@ export const splitNonEmptyAt: { export const split: { (n: number): (self: Iterable) => Array> (self: Iterable, n: number): Array> -} = dual(2, (self: Iterable, n: number) => { +} = dual(self: Iterable, n: number) => Array>>(2, (self, n) => { const input = fromIterable(self) return chunksOf(input, Math.ceil(input.length / Math.floor(n))) }) diff --git a/packages/effect/src/Function.ts b/packages/effect/src/Function.ts index a9f03e8a479..aeb6a296a23 100644 --- a/packages/effect/src/Function.ts +++ b/packages/effect/src/Function.ts @@ -69,14 +69,22 @@ export const isFunction = (input: unknown): input is Function => typeof input == * @since 2.0.0 */ export const dual: { - ) => any, DataFirst extends (...args: Array) => any>( - arity: Parameters["length"], - body: DataFirst - ): DataLast & DataFirst - ) => any, DataFirst extends (...args: Array) => any>( + < + Other extends (...args: ReadonlyArray) => any, + Impl extends (...args: ReadonlyArray) => any, + Signature extends Impl = Impl & ([Array] extends [Parameters] ? unknown : Other) + >( + arity: Parameters["length"], + body: Impl + ): Signature + < + Other extends (...args: ReadonlyArray) => any, + Impl extends (...args: ReadonlyArray) => any, + Signature extends Impl = Impl & ([Array] extends [Parameters] ? unknown : Other) + >( isDataFirst: (args: IArguments) => boolean, - body: DataFirst - ): DataLast & DataFirst + body: Impl + ): Signature } = function(arity, body) { if (typeof arity === "function") { return function() { diff --git a/packages/effect/src/Iterable.ts b/packages/effect/src/Iterable.ts index 58fe93b1ff3..855cf90edd1 100644 --- a/packages/effect/src/Iterable.ts +++ b/packages/effect/src/Iterable.ts @@ -376,9 +376,12 @@ export const findFirst: { (self: Iterable, f: (a: A, i: number) => Option): Option (self: Iterable, refinement: (a: A, i: number) => a is B): Option (self: Iterable, predicate: (a: A, i: number) => boolean): Option -} = dual( +} = dual< + typeof findFirst, + (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)) => Option +>( 2, - (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)): Option => { + (self, f) => { let i = 0 for (const a of self) { const o = f(a, i) @@ -410,7 +413,10 @@ export const findLast: { (self: Iterable, f: (a: A, i: number) => Option): Option (self: Iterable, refinement: (a: A, i: number) => a is B): Option (self: Iterable, predicate: (a: A, i: number) => boolean): Option -} = dual( +} = dual< + typeof findLast, + (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)) => Option +>( 2, (self: Iterable, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option)): Option => { let i = 0 @@ -795,7 +801,10 @@ export const filterMap: { export const filterMapWhile: { (f: (a: A, i: number) => Option): (self: Iterable) => Iterable (self: Iterable, f: (a: A, i: number) => Option): Iterable -} = dual(2, (self: Iterable, f: (a: A, i: number) => Option) => ({ +} = dual< + typeof filterMapWhile, + (self: Iterable, f: (a: A, i: number) => Option) => Iterable +>(2, (self, f) => ({ [Symbol.iterator]() { const iterator = self[Symbol.iterator]() let i = 0 diff --git a/packages/effect/src/Option.ts b/packages/effect/src/Option.ts index 01de0073f0a..c3f6f9fe676 100644 --- a/packages/effect/src/Option.ts +++ b/packages/effect/src/Option.ts @@ -614,7 +614,7 @@ export const map: { */ export const as: { (b: B): (self: Option) => Option -} = dual(2, (self: Option, b: B): Option => map(self, () => b)) +} = dual(self: Option, b: B) => Option>(2, (self, b) => map(self, () => b)) /** * Maps the `Some` value of this `Option` to the `void` constant value. diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index 27b328667fb..40b9e564a6b 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -495,10 +495,12 @@ export const andThen: { (f: Cause.Cause): (self: Cause.Cause) => Cause.Cause (self: Cause.Cause, f: (e: E) => Cause.Cause): Cause.Cause (self: Cause.Cause, f: Cause.Cause): Cause.Cause -} = dual( +} = dual< + typeof andThen, + (self: Cause.Cause, f: ((e: E) => Cause.Cause) | Cause.Cause) => Cause.Cause +>( 2, - (self: Cause.Cause, f: ((e: E) => Cause.Cause) | Cause.Cause): Cause.Cause => - isFunction(f) ? flatMap(self, f) : flatMap(self, () => f) + (self, f) => isFunction(f) ? flatMap(self, f) : flatMap(self, () => f) ) // ----------------------------------------------------------------------------- diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index 95b94436738..1f66442051a 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -1986,7 +1986,18 @@ export const forEach: { readonly discard: true } ): Effect.Effect -} = dual((args) => Predicate.isIterable(args[0]), ( +} = dual< + typeof forEach, + ( + self: Iterable, + f: (a: A, i: number) => Effect.Effect, + options?: { + readonly concurrency?: Concurrency | undefined + readonly batching?: boolean | "inherit" | undefined + readonly discard?: boolean | undefined + } + ) => Effect.Effect | Effect.Effect, E, R> | Effect.Effect, E, R> +>((args) => Predicate.isIterable(args[0]), ( self: Iterable, f: (a: A, i: number) => Effect.Effect, options?: { diff --git a/packages/effect/src/internal/hashSet.ts b/packages/effect/src/internal/hashSet.ts index a60b87d0758..9840f5bfeb9 100644 --- a/packages/effect/src/internal/hashSet.ts +++ b/packages/effect/src/internal/hashSet.ts @@ -280,18 +280,21 @@ export const filter: { (predicate: Predicate>): (self: HS.HashSet) => HS.HashSet (self: HS.HashSet, refinement: Refinement): HS.HashSet (self: HS.HashSet, predicate: Predicate): HS.HashSet -} = dual(2, (self: HS.HashSet, f: Predicate) => { - return mutate(empty(), (set) => { - const iterator = values(self) - let next: IteratorResult - while (!(next = iterator.next()).done) { - const value = next.value - if (f(value)) { - add(set, value) +} = dual(self: HS.HashSet, f: Predicate) => HS.HashSet>( + 2, + (self: HS.HashSet, f: Predicate): HS.HashSet => { + return mutate(empty(), (set) => { + const iterator = values(self) + let next: IteratorResult + while (!(next = iterator.next()).done) { + const value = next.value + if (f(value)) { + add(set, value) + } } - } - }) -}) + }) + } +) /** @internal */ export const partition: { diff --git a/packages/effect/src/internal/stm/tMap.ts b/packages/effect/src/internal/stm/tMap.ts index 2dead7c4244..500ca65ca4a 100644 --- a/packages/effect/src/internal/stm/tMap.ts +++ b/packages/effect/src/internal/stm/tMap.ts @@ -362,7 +362,16 @@ export const removeIf: { readonly discard: false } ): STM.STM> -} = dual((args) => isTMap(args[0]), ( +} = dual< + typeof removeIf, + ( + self: TMap.TMap, + predicate: (key: K, value: V) => boolean, + options?: { + readonly discard: boolean + } + ) => STM.STM | void> +>((args) => isTMap(args[0]), ( self: TMap.TMap, predicate: (key: K, value: V) => boolean, options?: { diff --git a/packages/effect/test/Function.test.ts b/packages/effect/test/Function.test.ts index 47ab6cb45a2..b752a7fb9e0 100644 --- a/packages/effect/test/Function.test.ts +++ b/packages/effect/test/Function.test.ts @@ -140,7 +140,7 @@ describe("Function", () => { deepStrictEqual(f(3, 2), 1) deepStrictEqual(Function.pipe(3, f(2)), 1) // should ignore excess arguments - deepStrictEqual(f.apply(null, [3, 2, 100] as any), 1) + deepStrictEqual((f as (self: number, that: number) => number).apply(null, [3, 2, 100] as any), 1) }) it("arity: 0", () => { @@ -169,7 +169,7 @@ describe("Function", () => { deepStrictEqual(f(3, 2), 1) deepStrictEqual(Function.pipe(3, f(2)), 1) // should ignore excess arguments - deepStrictEqual(f.apply(null, [3, 2, 100] as any), 1) + deepStrictEqual((f as (self: number, that: number) => number).apply(null, [3, 2, 100] as any), 1) }) it("arity: 5", () => { @@ -180,7 +180,13 @@ describe("Function", () => { deepStrictEqual(f("_", "a", "b", "c", "d"), "_abcd") deepStrictEqual(Function.pipe("_", f("a", "b", "c", "d")), "_abcd") // should ignore excess arguments - deepStrictEqual(f.apply(null, ["_", "a", "b", "c", "d", "e"] as any), "_abcd") + deepStrictEqual( + (f as ((self: string, a: string, b: string, c: string, d: string) => string)).apply( + null, + ["_", "a", "b", "c", "d", "e"] as any + ), + "_abcd" + ) }) it("arity > 5", () => { @@ -191,7 +197,13 @@ describe("Function", () => { deepStrictEqual(f("_", "a", "b", "c", "d", "e"), "_abcde") deepStrictEqual(Function.pipe("_", f("a", "b", "c", "d", "e")), "_abcde") // should ignore excess arguments - deepStrictEqual(f.apply(null, ["_", "a", "b", "c", "d", "e", "f"] as any), "_abcde") + deepStrictEqual( + (f as (self: string, a: string, b: string, c: string, d: string, e: string) => string).apply( + null, + ["_", "a", "b", "c", "d", "e", "f"] as any + ), + "_abcde" + ) }) }) }) diff --git a/packages/platform/src/Transferable.ts b/packages/platform/src/Transferable.ts index 2e2eebdafbc..eb8ff29cd32 100644 --- a/packages/platform/src/Transferable.ts +++ b/packages/platform/src/Transferable.ts @@ -86,9 +86,15 @@ export const schema: { self: Schema.Schema, f: (_: I) => Iterable ): Schema.Schema -} = dual(2, ( - self: Schema.Schema, - f: (_: I) => Iterable +} = dual< + typeof schema, + ( + self: Schema.Schema, + f: (_: I) => Iterable + ) => Schema.Schema +>(2, ( + self, + f ) => Schema.transformOrFail( Schema.encodedSchema(self), diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 7ab22bc6d82..43133e3e490 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -2244,12 +2244,15 @@ export const optional: { self: Schema.All extends S ? "you can't apply optional implicitly, use optional() instead" : S, options?: Options ): [undefined] extends [Options] ? optional : optionalWithOptions -} = dual((args) => isSchema(args[0]), (from, options) => { +} = (dual((args) => isSchema(args[0]), ( + from, + options +) => { // Note: `Schema.All extends S ? "you can't...` is used to prevent the case where `optional` is implicitly applied. // For example: `S.String.pipe(S.optional)` would result in `S.String` being inferred as `Schema.All`, // which is not the intended behavior. This is mostly an aesthetic consideration, so if it causes issues, we can remove it. return new PropertySignatureWithFromImpl(optionalPropertySignatureAST(from, options), from) -}) +})) as unknown as typeof optional /** * @since 0.67.0 @@ -3060,9 +3063,12 @@ export interface extend extend export const extend: { (that: That): (self: Self) => extend (self: Self, that: That): extend -} = dual( +} = dual< + typeof extend, + (self: Self, that: That) => extend +>( 2, - (self: Self, that: That) => make(extendAST(self.ast, that.ast, [])) + (self, that) => make(extendAST(self.ast, that.ast, [])) ) /** diff --git a/packages/typeclass/src/Filterable.ts b/packages/typeclass/src/Filterable.ts index 4aeb8784a86..cbfffe08c7a 100644 --- a/packages/typeclass/src/Filterable.ts +++ b/packages/typeclass/src/Filterable.ts @@ -103,7 +103,10 @@ export const filter: ( predicate: (a: A) => boolean ): Kind } = (Filterable: Filterable) => - dual( + dual< + ReturnType>, + (self: Kind, predicate: (a: A) => boolean) => Kind + >( 2, (self: Kind, predicate: (a: A) => boolean): Kind => Filterable.filterMap(self, (b) => (predicate(b) ? Option.some(b) : Option.none()))