Skip to content

Commit

Permalink
Merge pull request #4 from unsplash/fix-nullary-excess-checks
Browse files Browse the repository at this point in the history
Fix excess property checking for nullary sums
  • Loading branch information
samhh committed Jan 15, 2024
2 parents e177bdb + c76273c commit f546c0c
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 31 deletions.
60 changes: 42 additions & 18 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,36 @@ type Value<A> = A extends Sum.Member<any, infer B> ? B : never
// We're going to be defining a series of very similar-looking mapped types
// without abstraction owing to TypeScript's lack of support for higher-kinded
// types i.e. passing generics as type arguments.

type Eqs<A extends Sum.AnyMember> = {
readonly [B in A as Value<B> extends null ? never : Tag<B>]: Eq<Value<B>>
}

type Ords<A extends Sum.AnyMember> = {
readonly [B in A as Value<B> extends null ? never : Tag<B>]: Ord<Value<B>>
}

type Shows<A extends Sum.AnyMember> = {
readonly [B in A as Value<B> extends null ? never : Tag<B>]: Show<Value<B>>
}
//
// If a sum is nullary then the reduced input type would be `{}`, which
// unfortunately disables excess property checking, hence the conditional. We
// wrap the condition in tuples to avoid distributing over the union.
//
// It should be noted that excess property checking won't help us if the input
// object is first defined elsewhere and then provided to our function. This
// should be considered an unsafe, unsupported use of this API.

type Nullary = Sum.Member<string>

type Eqs<A extends Sum.AnyMember> = readonly [A] extends readonly [Nullary]
? Record<string, never>
: {
readonly [B in A as Value<B> extends null ? never : Tag<B>]: Eq<Value<B>>
}

type Ords<A extends Sum.AnyMember> = readonly [A] extends readonly [Nullary]
? Record<string, never>
: {
readonly [B in A as Value<B> extends null ? never : Tag<B>]: Ord<Value<B>>
}

type Shows<A extends Sum.AnyMember> = readonly [A] extends readonly [Nullary]
? Record<string, never>
: {
readonly [B in A as Value<B> extends null ? never : Tag<B>]: Show<
Value<B>
>
}

/**
* Given an `Eq` instance for each member of `A` for which there's a value,
Expand Down Expand Up @@ -66,8 +84,11 @@ export const getEq = <A extends Sum.AnyMember>(eqs: Eqs<A>): Eq<A> =>

const eq = eqs[xk as keyof typeof eqs]

// eslint-disable-next-line @typescript-eslint/no-explicit-any
return eq === undefined || eq.equals(xv as any, yv as any)
return (
eq === undefined ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(eq as unknown as Eq<Value<A>>).equals(xv as any, yv as any)
)
})

/**
Expand Down Expand Up @@ -109,8 +130,10 @@ export const getOrd = <A extends Sum.AnyMember>(ords: Ords<A>): Ord<A> =>

const ord = ords[xk as keyof typeof ords]

// eslint-disable-next-line @typescript-eslint/no-explicit-any
return ord === undefined ? 0 : ord.compare(xv as any, yv as any)
return ord === undefined
? 0
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
(ord as unknown as Ord<Value<A>>).compare(xv as any, yv as any)
})

/**
Expand Down Expand Up @@ -149,8 +172,9 @@ export const getShow = <A extends Sum.AnyMember>(shows: Shows<A>): Show<A> => ({
// defined a member for which the value actually can tangibly be `null`
// e.g. `Member<'Rain', number | null>`.
return k in shows
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
`${k} ${shows[k as keyof typeof shows].show(v as any)}`
? `${k} ${(shows[k as keyof typeof shows] as unknown as Show<Value<A>>)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.show(v as any)}`
: k
},
})
36 changes: 23 additions & 13 deletions test/type/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,32 @@ import * as Ord from "fp-ts/Ord"
import * as Show from "fp-ts/Show"
import * as Num from "fp-ts/number"

type A = Sum.Member<"A1"> | Sum.Member<"A2", number>
type NonNullary = Sum.Member<"A1"> | Sum.Member<"A2", number>
type Nullary = Sum.Member<"B1"> | Sum.Member<"B2">

//# getEq requires instances only for members with values
getEq<A>({ A2: Num.Eq }) // $ExpectType Eq<A>
getEq<A>({}) // $ExpectError
getEq<A>({ A2: Num.Eq as unknown as Eq.Eq<string> }) // $ExpectError
getEq<A>({ A1: Num.Eq, A2: Num.Eq }) // $ExpectError
getEq<NonNullary>({ A2: Num.Eq }) // $ExpectType Eq<NonNullary>
getEq<NonNullary>({}) // $ExpectError
getEq<NonNullary>({ A2: Num.Eq as unknown as Eq.Eq<string> }) // $ExpectError
getEq<NonNullary>({ A1: Num.Eq, A2: Num.Eq }) // $ExpectError

getEq<Nullary>({}) // $ExpectType Eq<Nullary>
getEq<Nullary>({ A1: Num.Eq }) // $ExpectError

//# getOrd requires instances only for members with values
getOrd<A>({ A2: Num.Ord }) // $ExpectType Ord<A>
getOrd<A>({}) // $ExpectError
getOrd<A>({ A2: Num.Ord as unknown as Ord.Ord<string> }) // $ExpectError
getOrd<A>({ A1: Num.Ord, A2: Num.Ord }) // $ExpectError
getOrd<NonNullary>({ A2: Num.Ord }) // $ExpectType Ord<NonNullary>
getOrd<NonNullary>({}) // $ExpectError
getOrd<NonNullary>({ A2: Num.Ord as unknown as Ord.Ord<string> }) // $ExpectError
getOrd<NonNullary>({ A1: Num.Ord, A2: Num.Ord }) // $ExpectError

getOrd<Nullary>({}) // $ExpectType Ord<Nullary>
getOrd<Nullary>({ A1: Num.Ord }) // $ExpectError

//# getShow requires instances only for members with values
getShow<A>({ A2: Num.Show }) // $ExpectType Show<A>
getShow<A>({}) // $ExpectError
getShow<A>({ A2: Num.Show as unknown as Show.Show<string> }) // $ExpectError
getShow<A>({ A1: Num.Show, A2: Num.Show }) // $ExpectError
getShow<NonNullary>({ A2: Num.Show }) // $ExpectType Show<NonNullary>
getShow<NonNullary>({}) // $ExpectError
getShow<NonNullary>({ A2: Num.Show as unknown as Show.Show<string> }) // $ExpectError
getShow<NonNullary>({ A1: Num.Show, A2: Num.Show }) // $ExpectError

getShow<Nullary>({}) // $ExpectType Show<Nullary>
getShow<Nullary>({ A1: Num.Show }) // $ExpectError

0 comments on commit f546c0c

Please sign in to comment.