Skip to content

Commit

Permalink
Struct: add get, closes #1890 (#1891)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Jan 10, 2024
1 parent 687e02e commit 8eec87e
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 40 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-carrots-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Types: add `MatchRecord`
5 changes: 5 additions & 0 deletions .changeset/dry-spies-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

fix `pick` behavior when the input is a record
5 changes: 5 additions & 0 deletions .changeset/rich-toys-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Struct: fix `pick` signature
5 changes: 5 additions & 0 deletions .changeset/small-beers-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Struct: add `get`
5 changes: 5 additions & 0 deletions .changeset/smooth-pets-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Struct: fix `omit` signature
170 changes: 169 additions & 1 deletion packages/effect/dtslint/Struct.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,176 @@
import { pipe } from "effect/Function"
import { hole, pipe } from "effect/Function"
import * as S from "effect/Struct"

const asym = Symbol.for("effect/dtslint/a")
const bsym = Symbol.for("effect/dtslint/b")
const csym = Symbol.for("effect/dtslint/c")
const dsym = Symbol.for("effect/dtslint/d")

declare const stringNumberRecord: Record<string, number>
declare const symbolNumberRecord: Record<symbol, number>
declare const numberNumberRecord: Record<number, number>
declare const templateLiteralNumberRecord: Record<`a${string}`, number>

const stringStruct = { a: "a", b: 1, c: true }
const symbolStruct = { [asym]: "a", [bsym]: 1, [csym]: true }
const numberStruct = { 1: "a", 2: 1, 3: true }

// -------------------------------------------------------------------------------------
// evolve
// -------------------------------------------------------------------------------------

// $ExpectType { a: boolean; }
S.evolve({ a: 1 }, { a: (x) => x > 0 })

// $ExpectType { a: number; b: number; }
pipe({ a: "a", b: 2 }, S.evolve({ a: (s) => s.length }))

// -------------------------------------------------------------------------------------
// get
// -------------------------------------------------------------------------------------

// @ts-expect-error
pipe({}, S.get("a"))

// $ExpectType string
pipe(stringStruct, S.get("a"))

// $ExpectType string
S.get("a")(stringStruct)

// $ExpectType number | undefined
pipe(stringNumberRecord, S.get("a"))

// $ExpectType <S extends Record<"a", any>>(s: S) => MatchRecord<S, S["a"] | undefined, S["a"]>
S.get("a")

// $ExpectType string
pipe(symbolStruct, S.get(asym))

// $ExpectType string
S.get(asym)(symbolStruct)

// $ExpectType number | undefined
pipe(symbolNumberRecord, S.get(asym))

// $ExpectType <S extends Record<typeof asym, any>>(s: S) => MatchRecord<S, S[typeof asym] | undefined, S[typeof asym]>
S.get(asym)

// $ExpectType string
pipe(numberStruct, S.get(1))

// $ExpectType string
S.get(1)(numberStruct)

// $ExpectType number | undefined
pipe(numberNumberRecord, S.get(1))

// $ExpectType <S extends Record<1, any>>(s: S) => MatchRecord<S, S[1] | undefined, S[1]>
S.get(1)

// $ExpectType number | undefined
pipe(templateLiteralNumberRecord, S.get("ab"))

// $ExpectType boolean
pipe(hole<Record<string, number> & { a: boolean }>(), S.get("a"))

// @ts-expect-error
pipe(hole<Record<string, number> & { a: boolean }>(), S.get("b"))

// -------------------------------------------------------------------------------------
// pick
// -------------------------------------------------------------------------------------

// @ts-expect-error
pipe(stringStruct, S.pick("d"))

// @ts-expect-error
S.pick("d")(stringStruct)

// $ExpectType { [x: string]: unknown; }
S.pick("d" as string)(stringStruct)

// $ExpectType { a: string; b: number; }
pipe(stringStruct, S.pick("a", "b"))

// $ExpectType { a: number | undefined; b: number | undefined; }
pipe(stringNumberRecord, S.pick("a", "b"))

// @ts-expect-error
pipe(symbolStruct, S.pick(dsym))

// @ts-expect-error
S.pick(dsym)(symbolStruct)

// $ExpectType { [x: symbol]: unknown; }
S.pick(dsym as symbol)(symbolStruct)

// $ExpectType { [asym]: string; [bsym]: number; }
pipe(symbolStruct, S.pick(asym, bsym))

// $ExpectType { [asym]: number | undefined; [bsym]: number | undefined; }
pipe(symbolNumberRecord, S.pick(asym, bsym))

// $ExpectType { 2: number; 1: string; }
pipe(numberStruct, S.pick(1, 2))

// @ts-expect-error
pipe(numberStruct, S.pick(4))

// @ts-expect-error
S.pick(4)(numberStruct)

// $ExpectType { [x: number]: unknown; }
S.pick(4 as number)(numberStruct)

// $ExpectType { 2: number | undefined; 1: number | undefined; }
pipe(numberNumberRecord, S.pick(1, 2))

// $ExpectType { ab: number | undefined; aa: number | undefined; }
pipe(templateLiteralNumberRecord, S.pick("aa", "ab"))

// $ExpectType { a: boolean; }
pipe(hole<Record<string, number> & { a: boolean }>(), S.pick("a"))

// @ts-expect-error
pipe(hole<Record<string, number> & { a: boolean }>(), S.pick("b"))

// -------------------------------------------------------------------------------------
// omit
// -------------------------------------------------------------------------------------

// @ts-expect-error
pipe(stringStruct, S.omit("d"))

// @ts-expect-error
S.omit("d")(stringStruct)

// $ExpectType { b: number; c: boolean; }
pipe(stringStruct, S.omit("a"))

// @ts-expect-error
pipe(symbolStruct, S.omit(dsym))

// @ts-expect-error
S.omit(dsym)(symbolStruct)

// $ExpectType { [bsym]: number; [csym]: boolean; }
pipe(symbolStruct, S.omit(asym))

// @ts-expect-error
pipe(numberStruct, S.omit(4))

// @ts-expect-error
S.omit(4)(numberStruct)

// $ExpectType { 2: number; 3: boolean; }
pipe(numberStruct, S.omit(1))

// $ExpectType { [x: string]: number; }
pipe(stringNumberRecord, S.omit("a"))

// $ExpectType { [x: symbol]: number; }
pipe(symbolNumberRecord, S.omit(asym))

// $ExpectType { [x: number]: number; }
pipe(numberNumberRecord, S.omit(1))
10 changes: 10 additions & 0 deletions packages/effect/dtslint/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,13 @@ export type MutableTuple = Types.Mutable<readonly [string, number]>

// $ExpectType { [x: string]: number; }
export type MutableRecord = Types.Simplify<Types.Mutable<{ readonly [_: string]: number }>>

// -------------------------------------------------------------------------------------
// MatchRecord
// -------------------------------------------------------------------------------------

// $ExpectType 1
export type MatchRecord1 = Types.MatchRecord<{ [x: string]: number }, 1, 0>

// $ExpectType 0
export type MatchRecord2 = Types.MatchRecord<{ a: number }, 1, 0>
30 changes: 24 additions & 6 deletions packages/effect/src/Struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import * as Equivalence from "./Equivalence.js"
import { dual } from "./Function.js"
import * as order from "./Order.js"
import type { Simplify } from "./Types.js"
import type { MatchRecord, Simplify } from "./Types.js"

/**
* Create a new object by picking properties of an existing object.
Expand All @@ -20,13 +20,15 @@ import type { Simplify } from "./Types.js"
*
* @since 2.0.0
*/
export const pick = <S, Keys extends readonly [keyof S, ...Array<keyof S>]>(
export const pick = <Keys extends Array<PropertyKey>>(
...keys: Keys
) =>
(s: S): Simplify<Pick<S, Keys[number]>> => {
<S extends Record<Keys[number], any>>(
s: S
): MatchRecord<S, { [K in Keys[number]]: S[K] | undefined }, { [K in Keys[number]]: S[K] }> => {
const out: any = {}
for (const k of keys) {
out[k] = s[k]
out[k] = (s as any)[k]
}
return out
}
Expand All @@ -42,10 +44,10 @@ export const pick = <S, Keys extends readonly [keyof S, ...Array<keyof S>]>(
*
* @since 2.0.0
*/
export const omit = <S, Keys extends readonly [keyof S, ...Array<keyof S>]>(
export const omit = <Keys extends Array<PropertyKey>>(
...keys: Keys
) =>
(s: S): Simplify<Omit<S, Keys[number]>> => {
<S extends Record<Keys[number], any>>(s: S): Simplify<Omit<S, Keys[number]>> => {
const out: any = { ...s }
for (const k of keys) {
delete out[k]
Expand Down Expand Up @@ -150,3 +152,19 @@ export const evolve: {
return out as any
}
)

/**
* Retrieves the value associated with the specified key from a struct.
*
* @example
* import * as Struct from "effect/Struct"
* import { pipe } from "effect/Function"
*
* const value = pipe({ a: 1, b: 2 }, Struct.get("a"))
*
* assert.deepStrictEqual(value, 1)
*
* @since 2.0.0
*/
export const get =
<K extends PropertyKey>(key: K) => <S extends Record<K, any>>(s: S): MatchRecord<S, S[K] | undefined, S[K]> => s[key]
5 changes: 5 additions & 0 deletions packages/effect/src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,8 @@ export type Covariant<A> = (_: never) => A
* @category models
*/
export type Contravariant<A> = (_: A) => void

/**
* @since 2.0.0
*/
export type MatchRecord<S, onTrue, onFalse> = {} extends S ? onTrue : onFalse
4 changes: 3 additions & 1 deletion packages/effect/test/Struct.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { assert, describe, expect, it } from "vitest"

describe("Struct", () => {
it("exports", () => {
expect(Struct.getOrder).exist
expect(Struct.getOrder).exist // alias of order.struct, tested there
})

it("pick", () => {
expect(pipe({ a: "a", b: 1, c: true }, Struct.pick("a", "b"))).toEqual({ a: "a", b: 1 })
const record: Record<string, number> = {}
expect(pipe(record, Struct.pick("a", "b"))).toStrictEqual({ a: undefined, b: undefined })
})

it("omit", () => {
Expand Down
Loading

0 comments on commit 8eec87e

Please sign in to comment.