diff --git a/.changeset/afraid-masks-own.md b/.changeset/afraid-masks-own.md new file mode 100644 index 0000000000..523f43eea0 --- /dev/null +++ b/.changeset/afraid-masks-own.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +remove ReadonlyRecord.fromIterable (duplicate of fromEntries) diff --git a/.changeset/four-turtles-suffer.md b/.changeset/four-turtles-suffer.md new file mode 100644 index 0000000000..e396aeb608 --- /dev/null +++ b/.changeset/four-turtles-suffer.md @@ -0,0 +1,7 @@ +--- +"effect": minor +"@effect/platform": minor +"@effect/typeclass": minor +--- + +add key type to ReadonlyRecord diff --git a/packages/effect/dtslint/ReadonlyRecord.ts b/packages/effect/dtslint/ReadonlyRecord.ts index bed8510b6b..1a331a373b 100644 --- a/packages/effect/dtslint/ReadonlyRecord.ts +++ b/packages/effect/dtslint/ReadonlyRecord.ts @@ -341,7 +341,7 @@ pipe(numbersOrStrings, ReadonlyRecord.partition(Predicate.isNumber)) // keys // ------------------------------------------------------------------------------------- -// $ExpectType string[] +// $ExpectType ("a" | "b")[] ReadonlyRecord.keys(structNumbers) // ------------------------------------------------------------------------------------- @@ -358,10 +358,24 @@ ReadonlyRecord.values(structNumbers) ReadonlyRecord.set(numbers, "a", true) // ------------------------------------------------------------------------------------- -// replace +// set // ------------------------------------------------------------------------------------- + // $ExpectType Record -ReadonlyRecord.replace(numbers, "a", true) +ReadonlyRecord.set(numbers, "a", true) + +// $ExpectType Record<"a" | "b" | "c", number | boolean> +ReadonlyRecord.set(structNumbers, "c", true) + +// ------------------------------------------------------------------------------------- +// remove +// ------------------------------------------------------------------------------------- + +// $ExpectType Record +ReadonlyRecord.remove(numbers, "a") + +// $ExpectType Record<"b", number> +ReadonlyRecord.remove(structNumbers, "a") // ------------------------------------------------------------------------------------- // reduce @@ -374,7 +388,7 @@ ReadonlyRecord.reduce(structNumbers, "", ( _value, // $ExpectType "a" | "b" key -) => key) +) => typeof key === "string" ? key : _acc) // ------------------------------------------------------------------------------------- // some @@ -432,9 +446,74 @@ ReadonlyRecord.every(structNumbers, ( ) => false) if (ReadonlyRecord.every(numbersOrStrings, Predicate.isString)) { - numbersOrStrings // $ExpectType Readonly> + numbersOrStrings // $ExpectType ReadonlyRecord } -if (ReadonlyRecord.every(Predicate.isString)(numbersOrStrings)) { - numbersOrStrings // $ExpectType Readonly> +if (ReadonlyRecord.every(numbersOrStrings, Predicate.isString)) { + numbersOrStrings // $ExpectType ReadonlyRecord +} + +// ------------------------------------------------------------------------------------- +// intersection +// ------------------------------------------------------------------------------------- + +// $ExpectType Record +ReadonlyRecord.intersection(numbers, numbersOrStrings, (a, _) => a) + +// $ExpectType Record +ReadonlyRecord.intersection(numbers, numbersOrStrings, (_, b) => b) + +// $ExpectType Record +ReadonlyRecord.intersection(structNumbers, structStrings, (_, b) => b) + +// $ExpectType Record +ReadonlyRecord.intersection(structNumbers, structStrings, (a, _) => a) + +// $ExpectType Record +ReadonlyRecord.intersection(numbers, numbers, (a, _) => a) + +// $ExpectType Record +ReadonlyRecord.intersection(numbers, structStrings, (a, _) => a) + +// $ExpectType Record +ReadonlyRecord.intersection(structNumbers, { + c: 2 +}, (a, _) => a) + +// $ExpectType Record<"b", number> +ReadonlyRecord.intersection(structNumbers, { + b: 2 +}, (a, _) => a) + +// ------------------------------------------------------------------------------------- +// has +// ------------------------------------------------------------------------------------- + +if (ReadonlyRecord.has(numbers, "a")) { + // $ExpectType Record + numbers } + +// @ts-expect-error +ReadonlyRecord.has(structNumbers, "c") + +// ------------------------------------------------------------------------------------- +// empty +// ------------------------------------------------------------------------------------- + +export const empty1: Record = ReadonlyRecord.empty() +export const empty2: Record = ReadonlyRecord.empty() +// @ts-expect-error +export const empty3: Record<"a", number> = ReadonlyRecord.empty() + +// $ExpectType Record +ReadonlyRecord.empty() + +// $ExpectType Record +ReadonlyRecord.empty<"a">() + +// $ExpectType Record<`a${string}bc`, never> +ReadonlyRecord.empty<`a${string}bc`>() + +// $ExpectType Record +ReadonlyRecord.empty() diff --git a/packages/effect/src/ReadonlyRecord.ts b/packages/effect/src/ReadonlyRecord.ts index b06ec776a7..a581ec8e9c 100644 --- a/packages/effect/src/ReadonlyRecord.ts +++ b/packages/effect/src/ReadonlyRecord.ts @@ -17,16 +17,39 @@ import type { NoInfer } from "./Types.js" * @category models * @since 2.0.0 */ -export interface ReadonlyRecord { - readonly [x: string]: A +export type ReadonlyRecord = { + readonly [P in K]: A +} + +/** + * @since 2.0.0 + */ +export declare namespace ReadonlyRecord { + type IsFiniteString = [T] extends [`${infer Head}${infer Rest}`] + ? string extends Head ? false : Rest extends "" ? true : IsFiniteString : + false + + /** + * @since 2.0.0 + */ + export type NonLiteralKey = K extends never ? never + : IsFiniteString extends true ? string + : K + + /** + * @since 2.0.0 + */ + export type IntersectKeys = [string] extends [K1 | K2] ? + NonLiteralKey & NonLiteralKey + : K1 & K2 } /** * @category type lambdas * @since 2.0.0 */ -export interface ReadonlyRecordTypeLambda extends TypeLambda { - readonly type: ReadonlyRecord +export interface ReadonlyRecordTypeLambda extends TypeLambda { + readonly type: ReadonlyRecord } /** @@ -35,7 +58,10 @@ export interface ReadonlyRecordTypeLambda extends TypeLambda { * @category constructors * @since 2.0.0 */ -export const empty = (): Record => ({}) +export const empty = (): Record< + ReadonlyRecord.NonLiteralKey, + V +> => ({} as any) /** * Determine if a record is empty. @@ -51,13 +77,8 @@ export const empty = (): Record => ({}) * @category guards * @since 2.0.0 */ -export const isEmptyRecord = (self: Record): self is Record => { - for (const k in self) { - if (has(self, k)) { - return false - } - } - return true +export const isEmptyRecord = (self: Record): self is Record => { + return keys(self).length === 0 } /** @@ -74,7 +95,9 @@ export const isEmptyRecord = (self: Record): self is Record(self: ReadonlyRecord) => self is ReadonlyRecord = isEmptyRecord +export const isEmptyReadonlyRecord: ( + self: ReadonlyRecord +) => self is ReadonlyRecord = isEmptyRecord /** * Takes an iterable and a projection function and returns a record. @@ -97,26 +120,49 @@ export const isEmptyReadonlyRecord: (self: ReadonlyRecord) => self is Read * @since 2.0.0 */ export const fromIterableWith: { - (f: (a: A) => readonly [string, B]): (self: Iterable) => Record - (self: Iterable, f: (a: A) => readonly [string, B]): Record -} = dual(2, (self: Iterable, f: (a: A) => readonly [string, B]): Record => { - const out: Record = {} - for (const a of self) { - const [k, b] = f(a) - out[k] = b + ( + f: (a: A) => readonly [K, B] + ): (self: Iterable) => Record, B> + ( + self: Iterable, + f: (a: A) => readonly [K, B] + ): Record, B> +} = dual( + 2, + ( + self: Iterable, + f: (a: A) => readonly [K, B] + ): Record, B> => { + const out: Record = empty() + for (const a of self) { + const [k, b] = f(a) + out[k] = b + } + return out } - return out -}) +) /** - * Creates a new record from an iterable collection of key/value pairs. + * Builds a record from an iterable of key-value pairs. + * + * If there are conflicting keys when using `fromEntries`, the last occurrence of the key/value pair will overwrite the + * previous ones. So the resulting record will only have the value of the last occurrence of each key. + * + * @param self - The iterable of key-value pairs. + * + * @example + * import { fromEntries } from "effect/ReadonlyRecord" + * + * const input: Array<[string, number]> = [["a", 1], ["b", 2]] + * + * assert.deepStrictEqual(fromEntries(input), { a: 1, b: 2 }) * * @since 2.0.0 * @category constructors */ -export const fromIterable: (entries: Iterable) => Record = fromIterableWith( - identity -) +export const fromEntries: ( + entries: Iterable +) => Record, Entry[1]> = Object.fromEntries /** * Creates a new record from an iterable, utilizing the provided function to determine the key for each element. @@ -143,28 +189,10 @@ export const fromIterable: (entries: Iterable) => Recor * @category constructors * @since 2.0.0 */ -export const fromIterableBy = (items: Iterable, f: (a: A) => string): Record => - fromIterableWith(items, (a) => [f(a), a]) - -/** - * Builds a record from an iterable of key-value pairs. - * - * If there are conflicting keys when using `fromEntries`, the last occurrence of the key/value pair will overwrite the - * previous ones. So the resulting record will only have the value of the last occurrence of each key. - * - * @param self - The iterable of key-value pairs. - * - * @example - * import { fromEntries } from "effect/ReadonlyRecord" - * - * const input: Array<[string, number]> = [["a", 1], ["b", 2]] - * - * assert.deepStrictEqual(fromEntries(input), { a: 1, b: 2 }) - * - * @category conversions - * @since 2.0.0 - */ -export const fromEntries: (self: Iterable) => Record = fromIterableWith(identity) +export const fromIterableBy = ( + items: Iterable, + f: (a: A) => K +): Record, A> => fromIterableWith(items, (a) => [f(a), a]) /** * Transforms the values of a record into an `Array` with a custom mapping function. @@ -182,13 +210,13 @@ export const fromEntries: (self: Iterable) => Record(f: (key: K, a: A) => B): (self: Record) => Array - (self: Record, f: (key: K, a: A) => B): Array + (f: (key: K, a: A) => B): (self: ReadonlyRecord) => Array + (self: ReadonlyRecord, f: (key: K, a: A) => B): Array } = dual( 2, - (self: ReadonlyRecord, f: (key: string, a: A) => B): Array => { + (self: ReadonlyRecord, f: (key: K, a: A) => B): Array => { const out: Array = [] - for (const key of Object.keys(self)) { + for (const key of keys(self)) { out.push(f(key, self[key])) } return out @@ -209,7 +237,7 @@ export const collect: { * @category conversions * @since 2.0.0 */ -export const toEntries: (self: Record) => Array<[K, A]> = collect(( +export const toEntries: (self: ReadonlyRecord) => Array<[K, A]> = collect(( key, value ) => [key, value]) @@ -226,7 +254,7 @@ export const toEntries: (self: Record) => Array<[K, A * * @since 2.0.0 */ -export const size = (self: ReadonlyRecord): number => Object.keys(self).length +export const size = (self: ReadonlyRecord): number => keys(self).length /** * Check if a given `key` exists in a record. @@ -235,19 +263,27 @@ export const size = (self: ReadonlyRecord): number => Object.keys(self).le * @param key - the key to look for in the record. * * @example - * import { has } from "effect/ReadonlyRecord" + * import { empty, has } from "effect/ReadonlyRecord" * * assert.deepStrictEqual(has({ a: 1, b: 2 }, "a"), true); - * assert.deepStrictEqual(has({ a: 1, b: 2 }, "c"), false); + * assert.deepStrictEqual(has(empty(), "c"), false); * * @since 2.0.0 */ export const has: { - (key: string): (self: ReadonlyRecord) => boolean - (self: ReadonlyRecord, key: string): boolean + ( + key: NoInfer + ): (self: ReadonlyRecord) => boolean + ( + self: ReadonlyRecord, + key: NoInfer + ): boolean } = dual( 2, - (self: ReadonlyRecord, key: string): boolean => Object.prototype.hasOwnProperty.call(self, key) + ( + self: ReadonlyRecord, + key: NoInfer + ): boolean => Object.prototype.hasOwnProperty.call(self, key) ) /** @@ -260,7 +296,7 @@ export const has: { * import { get } from "effect/ReadonlyRecord" * import { some, none } from "effect/Option" * - * const person = { name: "John Doe", age: 35 } + * const person: Record = { name: "John Doe", age: 35 } * * assert.deepStrictEqual(get(person, "name"), some("John Doe")) * assert.deepStrictEqual(get(person, "email"), none()) @@ -268,11 +304,12 @@ export const has: { * @since 2.0.0 */ export const get: { - (key: string): (self: ReadonlyRecord) => Option.Option - (self: ReadonlyRecord, key: string): Option.Option + (key: NoInfer): (self: ReadonlyRecord) => Option.Option + (self: ReadonlyRecord, key: NoInfer): Option.Option } = dual( 2, - (self: ReadonlyRecord, key: string): Option.Option => has(self, key) ? Option.some(self[key]) : Option.none() + (self: ReadonlyRecord, key: NoInfer): Option.Option => + has(self, key) ? Option.some(self[key]) : Option.none() ) /** @@ -294,21 +331,21 @@ export const get: { * { a: 6 } * ) * assert.deepStrictEqual( - * modify({ a: 3 }, 'b', f), + * modify({ a: 3 } as Record, 'b', f), * { a: 3 } * ) * * @since 2.0.0 */ export const modify: { - ( - key: string, + ( + key: NoInfer, f: (a: A) => B - ): (self: ReadonlyRecord) => Record - (self: ReadonlyRecord, key: string, f: (a: A) => B): Record + ): (self: ReadonlyRecord) => Record + (self: ReadonlyRecord, key: NoInfer, f: (a: A) => B): Record } = dual( 3, - (self: ReadonlyRecord, key: string, f: (a: A) => B): Record => { + (self: ReadonlyRecord, key: NoInfer, f: (a: A) => B): Record => { if (!has(self, key)) { return { ...self } } @@ -335,24 +372,33 @@ export const modify: { * some({ a: 6 }) * ) * assert.deepStrictEqual( - * modifyOption({ a: 3 }, 'b', f), + * modifyOption({ a: 3 } as Record, 'b', f), * none() * ) * * @since 2.0.0 */ export const modifyOption: { - (key: string, f: (a: A) => B): (self: ReadonlyRecord) => Option.Option> - (self: ReadonlyRecord, key: string, f: (a: A) => B): Option.Option> + ( + key: NoInfer, + f: (a: A) => B + ): (self: ReadonlyRecord) => Option.Option> + ( + self: ReadonlyRecord, + key: NoInfer, + f: (a: A) => B + ): Option.Option> } = dual( 3, - (self: ReadonlyRecord, key: string, f: (a: A) => B): Option.Option> => { + ( + self: ReadonlyRecord, + key: NoInfer, + f: (a: A) => B + ): Option.Option> => { if (!has(self, key)) { return Option.none() } - const out: Record = { ...self } - out[key] = f(self[key]) - return Option.some(out) + return Option.some({ ...self, [key]: f(self[key]) }) } ) @@ -364,28 +410,39 @@ export const modifyOption: { * @param b - The new value to replace the existing value with. * * @example - * import { replaceOption } from "effect/ReadonlyRecord" + * import { empty, replaceOption } from "effect/ReadonlyRecord" * import { some, none } from "effect/Option" * * assert.deepStrictEqual( * replaceOption({ a: 1, b: 2, c: 3 }, 'a', 10), * some({ a: 10, b: 2, c: 3 }) * ) - * assert.deepStrictEqual(replaceOption({}, 'a', 10), none()) + * assert.deepStrictEqual(replaceOption(empty(), 'a', 10), none()) * * @since 2.0.0 */ export const replaceOption: { - (key: string, b: B): (self: ReadonlyRecord) => Option.Option> - (self: ReadonlyRecord, key: string, b: B): Option.Option> + ( + key: NoInfer, + b: B + ): (self: ReadonlyRecord) => Option.Option> + ( + self: ReadonlyRecord, + key: NoInfer, + b: B + ): Option.Option> } = dual( 3, - (self: ReadonlyRecord, key: string, b: B): Option.Option> => - modifyOption(self, key, () => b) + ( + self: ReadonlyRecord, + key: NoInfer, + b: B + ): Option.Option> => modifyOption(self, key, () => b) ) /** - * Removes a key from a record and returns a new record + * If the given key exists in the record, returns a new record with the key removed, + * otherwise returns the original record. * * @param self - the record to remove the key from. * @param key - the key to remove from the record. @@ -398,13 +455,19 @@ export const replaceOption: { * @since 2.0.0 */ export const remove: { - (key: string): (self: ReadonlyRecord) => Record - (self: ReadonlyRecord, key: string): Record -} = dual(2, (self: ReadonlyRecord, key: string): Record => { - const out = { ...self } - delete out[key] - return out -}) + (key: X): (self: ReadonlyRecord) => Record, A> + (self: ReadonlyRecord, key: X): Record, A> +} = dual( + 2, + (self: ReadonlyRecord, key: X): Record, A> => { + if (!has(self, key)) { + return { ...self } + } + const out = { ...self } + delete out[key] + return out + } +) /** * Retrieves the value of the property with the given `key` from a record and returns an `Option` @@ -419,19 +482,24 @@ export const remove: { * import { some, none } from 'effect/Option' * * assert.deepStrictEqual(pop({ a: 1, b: 2 }, "a"), some([1, { b: 2 }])) - * assert.deepStrictEqual(pop({ a: 1, b: 2 }, "c"), none()) + * assert.deepStrictEqual(pop({ a: 1, b: 2 } as Record, "c"), none()) * * @category record * @since 2.0.0 */ export const pop: { - (key: string): (self: ReadonlyRecord) => Option.Option<[A, Record]> - (self: ReadonlyRecord, key: string): Option.Option<[A, Record]> -} = dual(2, ( - self: ReadonlyRecord, - key: string -): Option.Option<[A, Record]> => - has(self, key) ? Option.some([self[key], remove(self, key)]) : Option.none()) + ( + key: X + ): (self: ReadonlyRecord) => Option.Option<[A, Record, A>]> + ( + self: ReadonlyRecord, + key: X + ): Option.Option<[A, Record, A>]> +} = dual(2, ( + self: ReadonlyRecord, + key: X +): Option.Option<[A, Record, A>]> => + has(self, key) ? Option.some([self[key], remove(self as any, key)]) : Option.none()) /** * Maps a record into another record by applying a transformation function to each of its values. @@ -454,13 +522,13 @@ export const pop: { * @since 2.0.0 */ export const map: { - (f: (a: A, key: K) => B): (self: Record) => Record - (self: Record, f: (a: A, key: K) => B): Record + (f: (a: A, key: NoInfer) => B): (self: ReadonlyRecord) => Record + (self: ReadonlyRecord, f: (a: A, key: NoInfer) => B): Record } = dual( 2, - (self: ReadonlyRecord, f: (a: A, key: string) => B): Record => { - const out: Record = {} - for (const key of Object.keys(self)) { + (self: ReadonlyRecord, f: (a: A, key: NoInfer) => B): Record => { + const out: Record = { ...self } as any + for (const key of keys(self)) { out[key] = f(self[key], key) } return out @@ -479,13 +547,21 @@ export const map: { * @since 2.0.0 */ export const mapKeys: { - (f: (key: string, a: A) => string): (self: ReadonlyRecord) => Record - (self: ReadonlyRecord, f: (key: string, a: A) => string): Record + ( + f: (key: K, a: A) => K2 + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + f: (key: K, a: A) => K2 + ): Record } = dual( 2, - (self: ReadonlyRecord, f: (key: string, a: A) => string): Record => { - const out: Record = {} - for (const key of Object.keys(self)) { + ( + self: ReadonlyRecord, + f: (key: K, a: A) => K2 + ): Record => { + const out: Record = {} as any + for (const key of keys(self)) { const a = self[key] out[f(key, a)] = a } @@ -505,15 +581,23 @@ export const mapKeys: { * @since 2.0.0 */ export const mapEntries: { - (f: (a: A, key: string) => [string, A]): (self: ReadonlyRecord) => Record - (self: ReadonlyRecord, f: (a: A, key: string) => [string, A]): Record + ( + f: (a: A, key: K) => readonly [K2, B] + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + f: (a: A, key: K) => [K2, B] + ): Record } = dual( 2, - (self: ReadonlyRecord, f: (a: A, key: string) => [string, A]): Record => { - const out: Record = {} - for (const key of Object.keys(self)) { - const [k, a] = f(self[key], key) - out[k] = a + ( + self: ReadonlyRecord, + f: (a: A, key: K) => [K2, B] + ): Record => { + const out = > {} + for (const key of keys(self)) { + const [k, b] = f(self[key], key) + out[k] = b } return out } @@ -537,21 +621,29 @@ export const mapEntries: { * @since 2.0.0 */ export const filterMap: { - (f: (a: A, key: K) => Option.Option): (self: Record) => Record - (self: Record, f: (a: A, key: K) => Option.Option): Record -} = dual(2, ( - self: Record, - f: (a: A, key: string) => Option.Option -): Record => { - const out: Record = {} - for (const key of Object.keys(self)) { - const o = f(self[key], key) - if (Option.isSome(o)) { - out[key] = o.value + ( + f: (a: A, key: K) => Option.Option + ): (self: ReadonlyRecord) => Record, B> + ( + self: ReadonlyRecord, + f: (a: A, key: K) => Option.Option + ): Record, B> +} = dual( + 2, + ( + self: ReadonlyRecord, + f: (a: A, key: K) => Option.Option + ): Record, B> => { + const out: Record = empty() + for (const key of keys(self)) { + const o = f(self[key], key) + if (Option.isSome(o)) { + out[key] = o.value + } } + return out } - return out -}) +) /** * Selects properties from a record whose values match the given predicate. @@ -571,20 +663,26 @@ export const filterMap: { export const filter: { ( refinement: (a: NoInfer, key: K) => a is B - ): (self: Record) => Record + ): (self: ReadonlyRecord) => Record, B> ( predicate: (A: NoInfer, key: K) => boolean - ): (self: Record) => Record - (self: Record, refinement: (a: A, key: K) => a is B): Record - (self: Record, predicate: (a: A, key: K) => boolean): Record + ): (self: ReadonlyRecord) => Record, A> + ( + self: ReadonlyRecord, + refinement: (a: A, key: K) => a is B + ): Record, B> + ( + self: ReadonlyRecord, + predicate: (a: A, key: K) => boolean + ): Record, A> } = dual( 2, - ( - self: Record, - predicate: (a: A, key: string) => boolean - ): Record => { - const out: Record = {} - for (const key of Object.keys(self)) { + ( + self: ReadonlyRecord, + predicate: (a: A, key: K) => boolean + ): Record, A> => { + const out: Record = empty() + for (const key of keys(self)) { if (predicate(self[key], key)) { out[key] = self[key] } @@ -610,7 +708,9 @@ export const filter: { * @category filtering * @since 2.0.0 */ -export const getSomes: (self: ReadonlyRecord>) => Record = filterMap( +export const getSomes: ( + self: ReadonlyRecord> +) => Record, A> = filterMap( identity ) @@ -629,9 +729,11 @@ export const getSomes: (self: ReadonlyRecord>) => Record(self: ReadonlyRecord>): Record => { - const out: Record = {} - for (const key of Object.keys(self)) { +export const getLefts = ( + self: ReadonlyRecord> +): Record, E> => { + const out: Record = empty() + for (const key of keys(self)) { const value = self[key] if (E.isLeft(value)) { out[key] = value.left @@ -656,9 +758,11 @@ export const getLefts = (self: ReadonlyRecord>): Record(self: ReadonlyRecord>): Record => { - const out: Record = {} - for (const key of Object.keys(self)) { +export const getRights = ( + self: ReadonlyRecord> +): Record => { + const out: Record = empty() + for (const key of keys(self)) { const value = self[key] if (E.isRight(value)) { out[key] = value.right @@ -688,20 +792,22 @@ export const getRights = (self: ReadonlyRecord>): Record( f: (a: A, key: K) => Either - ): (self: Record) => [left: Record, right: Record] + ): ( + self: ReadonlyRecord + ) => [left: Record, B>, right: Record, C>] ( - self: Record, + self: ReadonlyRecord, f: (a: A, key: K) => Either - ): [left: Record, right: Record] + ): [left: Record, B>, right: Record, C>] } = dual( 2, - ( - self: Record, - f: (a: A, key: string) => Either - ): [left: Record, right: Record] => { - const left: Record = {} - const right: Record = {} - for (const key of Object.keys(self)) { + ( + self: ReadonlyRecord, + f: (a: A, key: K) => Either + ): [left: Record, B>, right: Record, C>] => { + const left: Record = empty() + const right: Record = empty() + for (const key of keys(self)) { const e = f(self[key], key) if (E.isLeft(e)) { left[key] = e.left @@ -731,9 +837,9 @@ export const partitionMap: { * @category filtering * @since 2.0.0 */ -export const separate: ( - self: ReadonlyRecord> -) => [Record, Record] = partitionMap(identity) +export const separate: ( + self: ReadonlyRecord> +) => [Record, A>, Record, B>] = partitionMap(identity) /** * Partitions a record into two separate records based on the result of a predicate function. @@ -754,28 +860,36 @@ export const separate: ( */ export const partition: { (refinement: (a: NoInfer, key: K) => a is B): ( - self: Record - ) => [excluded: Record>, satisfying: Record] + self: ReadonlyRecord + ) => [ + excluded: Record, Exclude>, + satisfying: Record, B> + ] ( predicate: (a: NoInfer, key: K) => boolean - ): (self: Record) => [excluded: Record, satisfying: Record] + ): ( + self: ReadonlyRecord + ) => [excluded: Record, A>, satisfying: Record, A>] ( - self: Record, + self: ReadonlyRecord, refinement: (a: A, key: K) => a is B - ): [excluded: Record>, satisfying: Record] + ): [ + excluded: Record, Exclude>, + satisfying: Record, B> + ] ( - self: Record, + self: ReadonlyRecord, predicate: (a: A, key: K) => boolean - ): [excluded: Record, satisfying: Record] + ): [excluded: Record, A>, satisfying: Record, A>] } = dual( 2, - ( - self: Record, - predicate: (a: A, key: string) => boolean - ): [excluded: Record, satisfying: Record] => { - const left: Record = {} - const right: Record = {} - for (const key of Object.keys(self)) { + ( + self: ReadonlyRecord, + predicate: (a: A, key: K) => boolean + ): [excluded: Record, A>, satisfying: Record, A>] => { + const left: Record = empty() + const right: Record = empty() + for (const key of keys(self)) { if (predicate(self[key], key)) { right[key] = self[key] } else { @@ -793,7 +907,7 @@ export const partition: { * * @since 2.0.0 */ -export const keys = (self: ReadonlyRecord): Array => Object.keys(self) +export const keys = (self: ReadonlyRecord): Array => Object.keys(self) as Array /** * Retrieve the values of a given record as an array. @@ -802,7 +916,7 @@ export const keys = (self: ReadonlyRecord): Array => Object.keys(s * * @since 2.0.0 */ -export const values = (self: ReadonlyRecord): Array => collect(self, (_, a) => a) +export const values = (self: ReadonlyRecord): Array => collect(self, (_, a) => a) /** * Add a new key-value pair or update an existing key's value in a record. @@ -820,16 +934,29 @@ export const values = (self: ReadonlyRecord): Array => collect(self, (_ * @since 2.0.0 */ export const set: { - (key: string, value: B): (self: ReadonlyRecord) => Record - (self: ReadonlyRecord, key: string, value: B): Record -} = dual(3, (self: ReadonlyRecord, key: string, value: B): Record => { - const out: Record = { ...self } - out[key] = value - return out -}) + ( + key: K1, + value: B + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + key: K1, + value: B + ): Record +} = dual( + 3, + ( + self: ReadonlyRecord, + key: K1, + value: B + ): Record => { + return { ...self, [key]: value } as any + } +) /** * Replace a key's value in a record and return the updated record. + * If the key does not exist in the record, the original record is returned. * * @param self - The original record. * @param key - The key to replace. @@ -845,15 +972,17 @@ export const set: { * @since 2.0.0 */ export const replace: { - (key: string, value: B): (self: ReadonlyRecord) => Record - (self: ReadonlyRecord, key: string, value: B): Record -} = dual(3, (self: ReadonlyRecord, key: string, value: B): Record => { - const out: Record = { ...self } - if (has(self, key)) { - out[key] = value + (key: NoInfer, value: B): (self: ReadonlyRecord) => Record + (self: ReadonlyRecord, key: NoInfer, value: B): Record +} = dual( + 3, + (self: ReadonlyRecord, key: NoInfer, value: B): Record => { + if (has(self, key)) { + return { ...self, [key]: value } + } + return { ...self } } - return out -}) +) /** * Check if all the keys and values in one record are also found in another record. @@ -865,11 +994,11 @@ export const replace: { * @since 2.0.0 */ export const isSubrecordBy = (equivalence: Equivalence): { - (that: ReadonlyRecord): (self: ReadonlyRecord) => boolean - (self: ReadonlyRecord, that: ReadonlyRecord): boolean + (that: ReadonlyRecord): (self: ReadonlyRecord) => boolean + (self: ReadonlyRecord, that: ReadonlyRecord): boolean } => - dual(2, (self: ReadonlyRecord, that: ReadonlyRecord): boolean => { - for (const key in self) { + dual(2, (self: ReadonlyRecord, that: ReadonlyRecord): boolean => { + for (const key of keys(self)) { if (!has(that, key) || !equivalence(self[key], that[key])) { return false } @@ -887,8 +1016,8 @@ export const isSubrecordBy = (equivalence: Equivalence): { * @since 2.0.0 */ export const isSubrecord: { - (that: ReadonlyRecord): (self: ReadonlyRecord) => boolean - (self: ReadonlyRecord, that: ReadonlyRecord): boolean + (that: ReadonlyRecord): (self: ReadonlyRecord) => boolean + (self: ReadonlyRecord, that: ReadonlyRecord): boolean } = isSubrecordBy(Equal.equivalence()) /** @@ -902,15 +1031,25 @@ export const isSubrecord: { * @since 2.0.0 */ export const reduce: { - (zero: Z, f: (accumulator: Z, value: V, key: K) => Z): (self: Record) => Z - (self: Record, zero: Z, f: (accumulator: Z, value: V, key: K) => Z): Z -} = dual(3, (self: Record, zero: Z, f: (accumulator: Z, value: V, key: string) => Z): Z => { - let out: Z = zero - for (const key in self) { - out = f(out, self[key], key) + ( + zero: Z, + f: (accumulator: Z, value: V, key: K) => Z + ): (self: ReadonlyRecord) => Z + (self: ReadonlyRecord, zero: Z, f: (accumulator: Z, value: V, key: K) => Z): Z +} = dual( + 3, + ( + self: ReadonlyRecord, + zero: Z, + f: (accumulator: Z, value: V, key: K) => Z + ): Z => { + let out: Z = zero + for (const key of keys(self)) { + out = f(out, self[key], key) + } + return out } - return out -}) +) /** * Check if all entries in a record meet a specific condition. @@ -923,24 +1062,27 @@ export const reduce: { export const every: { ( refinement: (value: A, key: K) => value is B - ): (self: Record) => self is Readonly> - (predicate: (value: A, key: K) => boolean): (self: Record) => boolean + ): (self: ReadonlyRecord) => self is ReadonlyRecord + (predicate: (value: A, key: K) => boolean): (self: ReadonlyRecord) => boolean + ( + self: ReadonlyRecord, + refinement: (value: A, key: K) => value is B + ): self is ReadonlyRecord + (self: ReadonlyRecord, predicate: (value: A, key: K) => boolean): boolean +} = dual( + 2, ( - self: Record, + self: ReadonlyRecord, refinement: (value: A, key: K) => value is B - ): self is Readonly> - (self: Record, predicate: (value: A, key: K) => boolean): boolean -} = dual(2, ( - self: Record, - refinement: (value: A, key: K) => value is B -): self is Readonly> => { - for (const key in self) { - if (!refinement(self[key], key)) { - return false + ): self is ReadonlyRecord => { + for (const key of keys(self)) { + if (!refinement(self[key], key)) { + return false + } } + return true } - return true -}) +) /** * Check if any entry in a record meets a specific condition. @@ -951,16 +1093,19 @@ export const every: { * @since 2.0.0 */ export const some: { - (predicate: (value: A, key: K) => boolean): (self: Record) => boolean - (self: Record, predicate: (value: A, key: K) => boolean): boolean -} = dual(2, (self: Record, predicate: (value: A, key: K) => boolean): boolean => { - for (const key in self) { - if (predicate(self[key], key)) { - return true + (predicate: (value: A, key: K) => boolean): (self: ReadonlyRecord) => boolean + (self: ReadonlyRecord, predicate: (value: A, key: K) => boolean): boolean +} = dual( + 2, + (self: ReadonlyRecord, predicate: (value: A, key: K) => boolean): boolean => { + for (const key of keys(self)) { + if (predicate(self[key], key)) { + return true + } } + return false } - return false -}) +) /** * Merge two records, preserving entries that exist in either of the records. @@ -972,37 +1117,37 @@ export const some: { * @since 2.0.0 */ export const union: { - ( - that: Record, - combine: (selfValue: V0, thatValue: V1) => V0 | V1 - ): (self: Record) => Record - ( - self: Record, - that: Record, - combine: (selfValue: V0, thatValue: V1) => V0 | V1 - ): Record + ( + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record } = dual( 3, - ( - self: Record, - that: Record, - combine: (selfValue: A, thatValue: A) => A - ): Record => { + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record => { if (isEmptyRecord(self)) { - return { ...that } + return { ...that } as any } if (isEmptyRecord(that)) { - return { ...self } + return { ...self } as any } - const out: Record = {} - for (const key in self) { - if (has(that, key)) { - out[key] = combine(self[key], that[key]) + const out: Record = empty() + for (const key of keys(self)) { + if (has(that, key as any)) { + out[key] = combine(self[key], that[key as unknown as K1]) } else { out[key] = self[key] } } - for (const key in that) { + for (const key of keys(that)) { if (!has(out, key)) { out[key] = that[key] } @@ -1021,25 +1166,29 @@ export const union: { * @since 2.0.0 */ export const intersection: { - ( - that: ReadonlyRecord, - combine: (selfValue: A, thatValue: A) => A - ): (self: ReadonlyRecord) => Record - (self: ReadonlyRecord, that: ReadonlyRecord, combine: (selfValue: A, thatValue: A) => A): Record + ( + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): (self: ReadonlyRecord) => Record, C> + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record, C> } = dual( 3, - ( - self: ReadonlyRecord, - that: ReadonlyRecord, - combine: (selfValue: A, thatValue: A) => A - ): Record => { + ( + self: ReadonlyRecord, + that: ReadonlyRecord, + combine: (selfValue: A, thatValue: B) => C + ): Record, C> => { + const out: Record = empty() if (isEmptyRecord(self) || isEmptyRecord(that)) { - return empty() + return out } - const out: Record = {} - for (const key in self) { - if (has(that, key)) { - out[key] = combine(self[key], that[key]) + for (const key of keys(self)) { + if (has(that, key as any)) { + out[key] = combine(self[key], that[key as unknown as K1]) } } return out @@ -1055,25 +1204,31 @@ export const intersection: { * @since 2.0.0 */ export const difference: { - ( - that: ReadonlyRecord - ): (self: ReadonlyRecord) => Record - (self: ReadonlyRecord, that: ReadonlyRecord): Record -} = dual(2, (self: ReadonlyRecord, that: ReadonlyRecord): Record => { + ( + that: ReadonlyRecord + ): (self: ReadonlyRecord) => Record + ( + self: ReadonlyRecord, + that: ReadonlyRecord + ): Record +} = dual(2, ( + self: ReadonlyRecord, + that: ReadonlyRecord +): Record => { if (isEmptyRecord(self)) { - return { ...that } + return { ...that } as any } if (isEmptyRecord(that)) { - return { ...self } + return { ...self } as any } - const out: Record = {} - for (const key in self) { - if (!has(that, key)) { + const out = > {} + for (const key of keys(self)) { + if (!has(that, key as any)) { out[key] = self[key] } } - for (const key in that) { - if (!has(self, key)) { + for (const key of keys(that)) { + if (!has(self, key as any)) { out[key] = that[key] } } @@ -1088,7 +1243,9 @@ export const difference: { * @category instances * @since 2.0.0 */ -export const getEquivalence = (equivalence: Equivalence): Equivalence> => { +export const getEquivalence = ( + equivalence: Equivalence +): Equivalence> => { const is = isSubrecordBy(equivalence) return (self, that) => is(self, that) && is(that, self) } @@ -1102,6 +1259,6 @@ export const getEquivalence = (equivalence: Equivalence): Equivalence(key: K, value: A): Record => (({ +export const singleton = (key: K, value: A): Record => ({ [key]: value -}) as Record) +} as any) diff --git a/packages/effect/test/ReadonlyRecord.test.ts b/packages/effect/test/ReadonlyRecord.test.ts index 6997c0c33a..4489700bd7 100644 --- a/packages/effect/test/ReadonlyRecord.test.ts +++ b/packages/effect/test/ReadonlyRecord.test.ts @@ -5,58 +5,65 @@ import * as Option from "effect/Option" import * as RR from "effect/ReadonlyRecord" import { assert, describe, expect, it } from "vitest" +const symA = Symbol.for("a") +const symB = Symbol.for("b") + describe("ReadonlyRecord", () => { it("get", () => { - expect(pipe({}, RR.get("a"))).toEqual(Option.none()) + expect(pipe(RR.empty(), RR.get("a"))).toEqual(Option.none()) expect(pipe({ a: 1 }, RR.get("a"))).toEqual(Option.some(1)) }) + it("replaceOption", () => { + expect(pipe(RR.empty(), RR.replaceOption("a", 2))).toEqual(Option.none()) + expect(pipe({ a: 1, [symA]: null }, RR.replaceOption("a", 2))).toEqual(Option.some({ a: 2, [symA]: null })) + expect(pipe({ a: 1, [symA]: null }, RR.replaceOption("a", true))).toEqual(Option.some({ a: true, [symA]: null })) + }) + it("modify", () => { - expect(pipe({}, RR.modify("a", (n: number) => n + 1))).toEqual({}) - expect(pipe({ a: 1 }, RR.modify("a", (n: number) => n + 1))).toEqual({ a: 2 }) - expect(pipe({ a: 1 }, RR.modify("a", (n: number) => String(n)))).toEqual( - { a: "1" } + expect(pipe(RR.empty(), RR.modify("a", (n: number) => n + 1))).toEqual({}) + expect(pipe({ a: 1, [symA]: null }, RR.modify("a", (n: number) => n + 1))).toEqual({ a: 2, [symA]: null }) + expect(pipe({ a: 1, [symA]: null }, RR.modify("a", (n: number) => String(n)))).toEqual( + { a: "1", [symA]: null } ) }) it("modifyOption", () => { - expect(pipe({}, RR.modifyOption("a", (n: number) => n + 1))).toEqual(Option.none()) - expect(pipe({ a: 1 }, RR.modifyOption("a", (n: number) => n + 1))).toEqual(Option.some({ a: 2 })) - expect(pipe({ a: 1 }, RR.modifyOption("a", (n: number) => String(n)))).toEqual( - Option.some({ a: "1" }) + expect(pipe(RR.empty(), RR.modifyOption("a", (n) => n + 1))).toEqual(Option.none()) + expect(pipe({ a: 1, [symA]: null }, RR.modifyOption("a", (n: number) => n + 1))).toEqual( + Option.some({ a: 2, [symA]: null }) + ) + expect(pipe({ a: 1, [symA]: null }, RR.modifyOption("a", (n: number) => String(n)))).toEqual( + Option.some({ a: "1", [symA]: null }) ) }) it("replaceOption", () => { - expect(pipe({}, RR.replaceOption("a", 2))).toEqual(Option.none()) - expect(pipe({ a: 1 }, RR.replaceOption("a", 2))).toEqual(Option.some({ a: 2 })) - expect(pipe({ a: 1 }, RR.replaceOption("a", true))).toEqual(Option.some({ a: true })) + expect(pipe(RR.empty(), RR.replaceOption("a", 2))).toEqual(Option.none()) + expect(pipe({ a: 1, [symA]: null }, RR.replaceOption("a", 2))).toEqual(Option.some({ a: 2, [symA]: null })) + expect(pipe({ a: 1, [symA]: null }, RR.replaceOption("a", true))).toEqual(Option.some({ a: true, [symA]: null })) }) it("map", () => { - expect(pipe({ a: 1, b: 2 }, RR.map((n) => n * 2))).toEqual({ a: 2, b: 4 }) - expect(pipe({ a: 1, b: 2 }, RR.map((n, k) => `${k}-${n}`))).toEqual({ - a: "a-1", - b: "b-2" + expect(pipe({ a: 1, b: 2, [symA]: null } as Record, RR.map((n) => n * 2))).toEqual({ + a: 2, + b: 4, + [symA]: null }) - }) - - it("fromIterable", () => { - const input = [["1", 2], ["2", 4], ["3", 6], ["4", 8]] as const - expect(RR.fromIterable(input)).toEqual({ - "1": 2, - "2": 4, - "3": 6, - "4": 8 + expect(pipe({ a: 1, b: 2, [symA]: null }, RR.map((n, k) => `${k}-${n}`))).toEqual({ + a: "a-1", + b: "b-2", + [symA]: null }) + expect(pipe({ [symA]: 1, [symB]: 2 }, RR.map((n) => n * 2))).toEqual({ [symA]: 1, [symB]: 2 }) }) it("fromIterableWith", () => { const input = [1, 2, 3, 4] - expect(RR.fromIterableWith(input, (a) => [String(a), a * 2])).toEqual({ + expect(RR.fromIterableWith(input, (a) => [a === 3 ? "a" : String(a), a * 2])).toEqual({ "1": 2, "2": 4, - "3": 6, + a: 6, "4": 8 }) }) @@ -73,35 +80,44 @@ describe("ReadonlyRecord", () => { }) it("fromEntries", () => { - const input: Array<[string, number]> = [["a", 1], ["b", 2]] - expect(RR.fromEntries(input)).toEqual({ a: 1, b: 2 }) + const input = [["1", 2], ["2", 4], ["3", 6], ["4", 8]] as const + expect(RR.fromEntries(input)).toEqual({ + "1": 2, + "2": 4, + "3": 6, + "4": 8 + }) }) it("collect", () => { - const x = { a: 1, b: 2, c: 3 } + const x = { a: 1, b: 2, c: 3, [symA]: null } assert.deepStrictEqual(RR.collect(x, (key, n) => [key, n]), [["a", 1], ["b", 2], ["c", 3]]) }) it("toEntries", () => { - const x = { a: 1, b: 2, c: 3 } + const x = { a: 1, b: 2, c: 3, [symA]: null } assert.deepStrictEqual(RR.toEntries(x), [["a", 1], ["b", 2], ["c", 3]]) }) it("remove", () => { - assert.deepStrictEqual(RR.remove({ a: 1, b: 2 }, "a"), { b: 2 }) - assert.deepStrictEqual(RR.remove({ a: 1, b: 2 }, "c"), { a: 1, b: 2 }) + assert.deepStrictEqual(RR.remove({ a: 1, b: 2, [symA]: null }, "a"), { b: 2, [symA]: null }) + assert.deepStrictEqual(RR.remove({ a: 1, b: 2, [symA]: null } as Record, "c"), { + a: 1, + b: 2, + [symA]: null + }) }) describe("pop", () => { it("should return the value associated with the given key, if the key is present in the record", () => { - const record = { a: 1, b: 2 } - const result = RR.pop("a")(record) + const record = { a: 1, b: 2, [symA]: null } + const result = RR.pop(record, "a") - assert.deepStrictEqual(result, Option.some([1, { b: 2 }] as [number, Record])) + assert.deepStrictEqual(result, Option.some([1, { b: 2, [symA]: null }] as [number, Record])) }) it("should return none if the key is not present in the record", () => { - const record = { a: 1, b: 2 } + const record = { a: 1, b: 2, [symA]: null } const result = RR.pop("c")(record) assert.deepStrictEqual(result, Option.none()) @@ -110,41 +126,41 @@ describe("ReadonlyRecord", () => { describe("filterMap", () => { it("should filter the properties of an object", () => { - const obj = { a: 1, b: 2, c: 3 } - const filtered = RR.filterMap(obj, (value, key) => (value > 2 ? Option.some(key) : Option.none())) + const x: Record = { a: 1, b: 2, c: 3, [symA]: null } + const filtered = RR.filterMap(x, (value, key) => (value > 2 ? Option.some(key) : Option.none())) expect(filtered).toEqual({ c: "c" }) }) }) - it("compact", () => { - const x = { a: Option.some(1), b: Option.none(), c: Option.some(2) } + it("getSomes", () => { + const x = { a: Option.some(1), b: Option.none(), c: Option.some(2), [symA]: null } assert.deepStrictEqual(RR.getSomes(x), { a: 1, c: 2 }) }) it("filter", () => { - const x = { a: 1, b: 2, c: 3, d: 4 } + const x: Record = { a: 1, b: 2, c: 3, d: 4, [symA]: null } assert.deepStrictEqual(RR.filter(x, (value) => value > 2), { c: 3, d: 4 }) }) it("partitionMap", () => { const f = (n: number) => (n > 2 ? Either.right(n + 1) : Either.left(n - 1)) assert.deepStrictEqual(RR.partitionMap({}, f), [{}, {}]) - assert.deepStrictEqual(RR.partitionMap({ a: 1, b: 3 }, f), [{ a: 0 }, { b: 4 }]) + assert.deepStrictEqual(RR.partitionMap({ a: 1, b: 3, [symA]: null }, f), [{ a: 0 }, { b: 4 }]) }) it("partition", () => { const f = (n: number) => n > 2 assert.deepStrictEqual(RR.partition({}, f), [{}, {}]) - assert.deepStrictEqual(RR.partition({ a: 1, b: 3 }, f), [{ a: 1 }, { b: 3 }]) + assert.deepStrictEqual(RR.partition({ a: 1, b: 3, [symA]: null }, f), [{ a: 1 }, { b: 3 }]) }) it("separate", () => { assert.deepStrictEqual( - RR.separate({ a: Either.left("e"), b: Either.right(1) }), + RR.separate({ a: Either.left("e"), b: Either.right(1), [symA]: null }), [{ a: "e" }, { b: 1 }] ) // should ignore non own properties - const o: RR.ReadonlyRecord> = Object.create({ a: 1 }) + const o: RR.ReadonlyRecord<"a", Either.Either> = Object.create({ a: 1 }) assert.deepStrictEqual(pipe(o, RR.separate), [{}, {}]) }) @@ -154,85 +170,95 @@ describe("ReadonlyRecord", () => { it("isEmptyRecord", () => { assert.deepStrictEqual(RR.isEmptyRecord({}), true) + assert.deepStrictEqual(RR.isEmptyRecord({ [symA]: null }), true) assert.deepStrictEqual(RR.isEmptyRecord({ a: 3 }), false) }) it("isEmptyReadonlyRecord", () => { assert.deepStrictEqual(RR.isEmptyReadonlyRecord({}), true) + assert.deepStrictEqual(RR.isEmptyReadonlyRecord({ [symA]: null }), true) assert.deepStrictEqual(RR.isEmptyReadonlyRecord({ a: 3 }), false) }) it("size", () => { - assert.deepStrictEqual(RR.size({ a: "a", b: 1, c: true }), 3) + assert.deepStrictEqual(RR.size({ a: "a", b: 1, c: true, [symA]: null }), 3) }) it("has", () => { - assert.deepStrictEqual(RR.has({ a: 1, b: 2 }, "a"), true) - assert.deepStrictEqual(RR.has({ a: 1, b: 2 }, "c"), false) + assert.deepStrictEqual(RR.has({ a: 1, b: 2, [symA]: null }, "a"), true) + assert.deepStrictEqual(RR.has({ a: 1, b: 2, [symA]: null } as Record, "c"), false) }) it("keys", () => { - assert.deepStrictEqual(RR.keys({ a: 1, b: 2 }), ["a", "b"]) + assert.deepStrictEqual(RR.keys({ a: 1, b: 2, [symA]: null }), ["a", "b"]) }) it("values", () => { - assert.deepStrictEqual(RR.values({ a: 1, b: 2 }), [1, 2]) + assert.deepStrictEqual(RR.values({ a: 1, b: 2, [symA]: null }), [1, 2]) }) it("set", () => { - assert.deepStrictEqual(RR.set({ a: 1, b: 2 }, "c", 3), { a: 1, b: 2, c: 3 }) - assert.deepStrictEqual(RR.set({ a: 1, b: 2 }, "a", 3), { a: 3, b: 2 }) + assert.deepStrictEqual(RR.set({ a: 1, b: 2, [symA]: null }, "c", 3), { a: 1, b: 2, c: 3, [symA]: null }) + assert.deepStrictEqual(RR.set({ a: 1, b: 2, [symA]: null }, "a", 3), { a: 3, b: 2, [symA]: null }) }) it("replace", () => { - expect(RR.replace({ a: 1, b: 2 }, "c", 3)).toStrictEqual({ a: 1, b: 2 }) - expect(RR.replace({ a: 1, b: 2 }, "a", 3)).toStrictEqual({ a: 3, b: 2 }) + expect(RR.replace({ a: 1, b: 2, [symA]: null } as Record, "c", 3)).toStrictEqual({ + a: 1, + b: 2, + [symA]: null + }) + expect(RR.replace({ a: 1, b: 2, [symA]: null }, "a", 3)).toStrictEqual({ a: 3, b: 2, [symA]: null }) }) it("isSubrecord", () => { - expect(RR.isSubrecord({}, {})).toBe(true) - expect(RR.isSubrecord({}, { a: 1 })).toBe(true) + expect(RR.isSubrecord(RR.empty(), {})).toBe(true) + expect(RR.isSubrecord(RR.empty(), { a: 1 })).toBe(true) expect(RR.isSubrecord({ a: 1 }, { a: 1 })).toBe(true) - expect(RR.isSubrecord({ a: 1 }, { a: 1, b: 2 })).toBe(true) + expect(RR.isSubrecord({ a: 1, [symA]: null }, { a: 1 })).toBe(true) + expect(RR.isSubrecord({ a: 1 }, { a: 1, [symA]: null })).toBe(true) + expect(RR.isSubrecord({ a: 1 } as Record, { a: 1, b: 2 })).toBe(true) expect(RR.isSubrecord({ b: 2, a: 1 }, { a: 1, b: 2 })).toBe(true) expect(RR.isSubrecord({ a: 1 }, { a: 2 })).toBe(false) - expect(RR.isSubrecord({ b: 2 }, { a: 1 })).toBe(false) + expect(RR.isSubrecord({ b: 2 } as Record, { a: 1 })).toBe(false) }) it("reduce", () => { // data-first assert.deepStrictEqual( - RR.reduce({ k1: "a", k2: "b" }, "-", (accumulator, value, key) => accumulator + key + value), + RR.reduce({ k1: "a", k2: "b", [symA]: null }, "-", (accumulator, value, key) => accumulator + key + value), "-k1ak2b" ) // data-last assert.deepStrictEqual( - pipe({ k1: "a", k2: "b" }, RR.reduce("-", (accumulator, value, key) => accumulator + key + value)), + pipe({ k1: "a", k2: "b", [symA]: null }, RR.reduce("-", (accumulator, value, key) => accumulator + key + value)), "-k1ak2b" ) }) it("every", () => { - assert.deepStrictEqual(RR.every((n: number) => n <= 2)({ a: 1, b: 2 }), true) - assert.deepStrictEqual(RR.every((n: number) => n <= 1)({ a: 1, b: 2 }), false) + assert.deepStrictEqual(RR.every((n: number) => n <= 2)({ a: 1, b: 2, [symA]: null }), true) + assert.deepStrictEqual(RR.every((n: number) => n <= 1)({ a: 1, b: 2, [symA]: null }), false) }) it("some", () => { - assert.deepStrictEqual(RR.some((n: number) => n <= 1)({ a: 1, b: 2 }), true) - assert.deepStrictEqual(RR.some((n: number) => n <= 0)({ a: 1, b: 2 }), false) + assert.deepStrictEqual(RR.some((n: number) => n <= 1)({ a: 1, b: 2, [symA]: null }), true) + assert.deepStrictEqual(RR.some((n: number) => n <= 0)({ a: 1, b: 2, [symA]: null }), false) }) it("union", () => { const combine = (s1: string, s2: string) => s1 + s2 - const x: RR.ReadonlyRecord = { + const x: RR.ReadonlyRecord = { a: "a1", b: "b1", - c: "c1" + c: "c1", + [symA]: null } - const y: RR.ReadonlyRecord = { + const y: RR.ReadonlyRecord = { b: "b2", c: "c2", - d: "d2" + d: "d2", + [symA]: null } assert.deepStrictEqual(RR.union(x, {}, combine), x) assert.deepStrictEqual(RR.union({}, x, combine), x) @@ -248,15 +274,17 @@ describe("ReadonlyRecord", () => { it("intersection", () => { const combine = (s1: string, s2: string) => s1 + s2 - const x: RR.ReadonlyRecord = { + const x: RR.ReadonlyRecord = { a: "a1", b: "b1", - c: "c1" + c: "c1", + [symA]: null } - const y: RR.ReadonlyRecord = { + const y: RR.ReadonlyRecord = { b: "b2", c: "c2", - d: "d2" + d: "d2", + [symA]: null } assert.deepStrictEqual(RR.intersection(x, {}, combine), {}) assert.deepStrictEqual(RR.intersection({}, y, combine), {}) @@ -267,15 +295,17 @@ describe("ReadonlyRecord", () => { }) it("difference", () => { - const x: RR.ReadonlyRecord = { + const x: RR.ReadonlyRecord = { a: "a1", b: "b1", - c: "c1" + c: "c1", + [symA]: null } - const y: RR.ReadonlyRecord = { + const y: RR.ReadonlyRecord = { b: "b2", c: "c2", - d: "d2" + d: "d2", + [symA]: null } assert.deepStrictEqual(RR.difference({}, x), x) assert.deepStrictEqual(RR.difference(x, {}), x) @@ -289,6 +319,7 @@ describe("ReadonlyRecord", () => { it("getEquivalence", () => { assert.deepStrictEqual(RR.getEquivalence(N.Equivalence)({ a: 1 }, { a: 1 }), true) + assert.deepStrictEqual(RR.getEquivalence(N.Equivalence)({ a: 1 }, { a: 1, [symA]: null }), true) assert.deepStrictEqual(RR.getEquivalence(N.Equivalence)({ a: 1 }, { a: 2 }), false) assert.deepStrictEqual(RR.getEquivalence(N.Equivalence)({ a: 1 }, { b: 1 }), false) const noPrototype = Object.create(null) @@ -300,12 +331,21 @@ describe("ReadonlyRecord", () => { }) it("mapKeys", () => { - expect(pipe({ a: 1, b: 2 }, RR.mapKeys((key) => key.toUpperCase()))).toStrictEqual({ A: 1, B: 2 }) - expect(RR.mapKeys({ a: 1, b: 2 }, (k) => k.toUpperCase())).toStrictEqual({ A: 1, B: 2 }) + expect(pipe({ a: 1, b: 2, [symA]: null }, RR.mapKeys((key) => key.toUpperCase()))).toStrictEqual({ + A: 1, + B: 2 + }) }) it("mapEntries", () => { - expect(pipe({ a: 1, b: 2 }, RR.mapEntries((a, key) => [key.toUpperCase(), a + 1]))).toStrictEqual({ A: 2, B: 3 }) - expect(RR.mapEntries({ a: 1, b: 2 }, (a, k) => [k.toUpperCase(), a + 1])).toStrictEqual({ A: 2, B: 3 }) + expect( + pipe( + { a: 1, b: 2, [symA]: null } as Record, + RR.mapEntries((a, key) => [key.toUpperCase(), a + 1]) + ) + ).toStrictEqual({ + A: 2, + B: 3 + }) }) }) diff --git a/packages/platform/src/Http/Headers.ts b/packages/platform/src/Http/Headers.ts index c819302b4f..bb2cd6f700 100644 --- a/packages/platform/src/Http/Headers.ts +++ b/packages/platform/src/Http/Headers.ts @@ -1,7 +1,6 @@ /** * @since 1.0.0 */ -import type * as Brand from "effect/Brand" import { dual } from "effect/Function" import type * as Option from "effect/Option" import * as ReadonlyArray from "effect/ReadonlyArray" @@ -24,13 +23,16 @@ export type HeadersTypeId = typeof HeadersTypeId * @since 1.0.0 * @category models */ -export type Headers = Brand.Branded, HeadersTypeId> +export interface Headers { + readonly [HeadersTypeId]: HeadersTypeId + readonly [key: string]: string +} /** * @since 1.0.0 * @category models */ -export type Input = ReadonlyRecord.ReadonlyRecord | Iterable +export type Input = ReadonlyRecord.ReadonlyRecord | Iterable /** * @since 1.0.0 @@ -60,7 +62,7 @@ export const fromInput: (input?: Input) => Headers = (input) => { * @since 1.0.0 * @category constructors */ -export const unsafeFromRecord = (input: ReadonlyRecord.ReadonlyRecord): Headers => input as Headers +export const unsafeFromRecord = (input: ReadonlyRecord.ReadonlyRecord): Headers => input as Headers /** * @since 1.0.0 @@ -72,7 +74,7 @@ export const has: { } = dual< (key: string) => (self: Headers) => boolean, (self: Headers, key: string) => boolean ->(2, (self, key) => ReadonlyRecord.has(self, key.toLowerCase())) +>(2, (self, key) => ReadonlyRecord.has(self as Record, key.toLowerCase())) /** * @since 1.0.0 @@ -84,7 +86,7 @@ export const get: { } = dual< (key: string) => (self: Headers) => Option.Option, (self: Headers, key: string) => Option.Option ->(2, (self, key) => ReadonlyRecord.get(self, key.toLowerCase())) +>(2, (self, key) => ReadonlyRecord.get(self as Record, key.toLowerCase())) /** * @since 1.0.0 diff --git a/packages/rpc/src/Rpc.ts b/packages/rpc/src/Rpc.ts index 32cd73c100..17b4b4aad8 100644 --- a/packages/rpc/src/Rpc.ts +++ b/packages/rpc/src/Rpc.ts @@ -309,7 +309,7 @@ export const annotateHeaders: { * @since 1.0.0 * @category headers */ -export const schemaHeaders = , A>( +export const schemaHeaders = , A>( schema: Schema.Schema ): Effect.Effect => { const decode = Schema.decodeUnknown(schema) diff --git a/packages/typeclass/src/data/ReadonlyRecord.ts b/packages/typeclass/src/data/ReadonlyRecord.ts index 4be5c16ca1..be3f8ad8bf 100644 --- a/packages/typeclass/src/data/ReadonlyRecord.ts +++ b/packages/typeclass/src/data/ReadonlyRecord.ts @@ -13,27 +13,20 @@ import type * as invariant from "../Invariant.js" import type * as traversable from "../Traversable.js" import type * as traversableFilterable from "../TraversableFilterable.js" -const map = ReadonlyRecord.map - -const imap = covariant.imap(map) - -const partitionMap = ReadonlyRecord.partitionMap - -const filterMap = ReadonlyRecord.filterMap - -const traverse = (F: applicative.Applicative): { +/** @internal */ +export const traverse = (F: applicative.Applicative): { ( f: (a: A, key: K) => Kind - ): (self: Record) => Kind> + ): (self: Record) => Kind> ( self: Record, f: (a: A, key: K) => Kind - ): Kind> + ): Kind> } => - dual(2, ( + dual(2, ( self: Record, f: (a: A, key: string) => Kind - ): Kind> => + ): Kind> => F.map( F.productAll( Object.entries(self).map(([key, a]) => F.map(f(a, key), (b) => [key, b] as const)) @@ -46,18 +39,45 @@ const traversePartitionMap = ( ): { ( f: (a: A) => Kind> - ): ( - self: ReadonlyRecord.ReadonlyRecord - ) => Kind, Record]> - ( - self: ReadonlyRecord.ReadonlyRecord, + ): ( + self: ReadonlyRecord.ReadonlyRecord + ) => Kind< + F, + R, + O, + E, + [ + Record, B>, + Record, C> + ] + > + ( + self: ReadonlyRecord.ReadonlyRecord, f: (a: A) => Kind> - ): Kind, Record]> + ): Kind< + F, + R, + O, + E, + [ + Record, B>, + Record, C> + ] + > } => - dual(2, ( - self: ReadonlyRecord.ReadonlyRecord, + dual(2, ( + self: ReadonlyRecord.ReadonlyRecord, f: (a: A) => Kind> - ): Kind, Record]> => { + ): Kind< + F, + R, + O, + E, + [ + Record, B>, + Record, C> + ] + > => { return F.map(traverse(F)(self, f), ReadonlyRecord.separate) }) @@ -66,60 +86,120 @@ const traverseFilterMap = ( ): { ( f: (a: A) => Kind> - ): (self: ReadonlyRecord.ReadonlyRecord) => Kind> - ( - self: ReadonlyRecord.ReadonlyRecord, + ): ( + self: ReadonlyRecord.ReadonlyRecord + ) => Kind, B>> + ( + self: ReadonlyRecord.ReadonlyRecord, f: (a: A) => Kind> - ): Kind> + ): Kind, B>> } => - dual(2, ( - self: ReadonlyRecord.ReadonlyRecord, + dual(2, ( + self: ReadonlyRecord.ReadonlyRecord, f: (a: A) => Kind> - ): Kind> => { + ): Kind, B>> => { return F.map(traverse(F)(self, f), ReadonlyRecord.getSomes) }) +const _map: covariant.Covariant>["map"] = ReadonlyRecord.map + +const _imap = covariant.imap>(_map) + +const _partitionMap: filterable.Filterable>["partitionMap"] = + ReadonlyRecord.partitionMap + +const _filterMap: filterable.Filterable>["filterMap"] = + ReadonlyRecord.filterMap + +const _traverse: traversable.Traversable>["traverse"] = traverse + +const _traversePartitionMap: traversableFilterable.TraversableFilterable< + ReadonlyRecord.ReadonlyRecordTypeLambda +>["traversePartitionMap"] = traversePartitionMap + +const _traverseFilterMap: traversableFilterable.TraversableFilterable< + ReadonlyRecord.ReadonlyRecordTypeLambda +>["traverseFilterMap"] = traverseFilterMap + +/** + * @category instances + * @since 1.0.0 + */ +export const getCovariant = (): covariant.Covariant< + ReadonlyRecord.ReadonlyRecordTypeLambda +> => ({ + imap: _imap, + map: _map +}) + +/** + * @category instances + * @since 1.0.0 + */ +export const Covariant = getCovariant() + +/** + * @category instances + * @since 1.0.0 + */ +export const getInvariant = (): invariant.Invariant< + ReadonlyRecord.ReadonlyRecordTypeLambda +> => ({ + imap: _imap +}) + +/** + * @category instances + * @since 1.0.0 + */ +export const Invariant = getInvariant() + +/** + * @category instances + * @since 1.0.0 + */ +export const getFilterable = (): filterable.Filterable< + ReadonlyRecord.ReadonlyRecordTypeLambda +> => ({ + partitionMap: _partitionMap, + filterMap: _filterMap +}) + /** * @category instances * @since 1.0.0 */ -export const Covariant: covariant.Covariant = { - imap, - map -} +export const Filterable = getFilterable() /** * @category instances * @since 1.0.0 */ -export const Invariant: invariant.Invariant = { - imap -} +export const getTraversable = (): traversable.Traversable< + ReadonlyRecord.ReadonlyRecordTypeLambda +> => ({ + traverse: _traverse +}) /** * @category instances * @since 1.0.0 */ -export const Filterable: filterable.Filterable = { - partitionMap, - filterMap -} +export const Traversable = getTraversable() /** * @category instances * @since 1.0.0 */ -export const Traversable: traversable.Traversable = { - traverse -} +export const getTraversableFilterable = (): traversableFilterable.TraversableFilterable< + ReadonlyRecord.ReadonlyRecordTypeLambda +> => ({ + traversePartitionMap: _traversePartitionMap, + traverseFilterMap: _traverseFilterMap +}) /** * @category instances * @since 1.0.0 */ -export const TraversableFilterable: traversableFilterable.TraversableFilterable< - ReadonlyRecord.ReadonlyRecordTypeLambda -> = { - traversePartitionMap, - traverseFilterMap -} +export const TraversableFilterable = getTraversableFilterable() diff --git a/packages/typeclass/test/data/ReadonlyRecord.test.ts b/packages/typeclass/test/data/ReadonlyRecord.test.ts new file mode 100644 index 0000000000..4d9a091c39 --- /dev/null +++ b/packages/typeclass/test/data/ReadonlyRecord.test.ts @@ -0,0 +1,43 @@ +import * as OptionInstances from "@effect/typeclass/data/Option" +import * as ReadonlyRecordInstances from "@effect/typeclass/data/ReadonlyRecord" +import * as Option from "effect/Option" +import { describe, expect, it } from "vitest" + +describe.concurrent("ReadonlyRecord", () => { + it("traverse (string)", () => { + const traverse = ReadonlyRecordInstances.traverse(OptionInstances.Applicative) + const stringRecord: Record = { + a: 1, + b: 2 + } + expect(traverse(stringRecord, (a, k) => Option.some(a + k))).toStrictEqual(Option.some({ + a: "1a", + b: "2b" + })) + expect(traverse(stringRecord, (a) => a < 1 ? Option.some(a) : Option.none())).toStrictEqual(Option.none()) + }) + + it("traverse (template literal)", () => { + const traverse = ReadonlyRecordInstances.getTraversable<`a${string}`>().traverse(OptionInstances.Applicative) + const templateLiteralRecord: Record<`a${string}`, number> = { + a: 1, + ab: 2 + } + expect(traverse(templateLiteralRecord, (a) => Option.some(a))).toStrictEqual(Option.some({ + a: 1, + ab: 2 + })) + expect(traverse(templateLiteralRecord, (a) => a < 1 ? Option.some(a) : Option.none())).toStrictEqual(Option.none()) + }) + + it("traverse (symbol)", () => { + const traverse = ReadonlyRecordInstances.traverse(OptionInstances.Applicative) + const a = Symbol.for("a") + const b = Symbol.for("b") + const symbolRecord: Record = { + [a]: 1, + [b]: 2 + } + expect(traverse(symbolRecord, (a) => Option.some(a))).toStrictEqual(Option.some({})) + }) +})