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

Improved typing for union types #2151

Merged
merged 12 commits into from
Mar 1, 2024
66 changes: 61 additions & 5 deletions __tests__/core/type-system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,21 @@ type DifferingKeys<ActualT, ExpectedT> = {
}[keyof ActualT | keyof ExpectedT] &
string

type InexactErrorMessage<ActualT, ExpectedT> = `Mismatched property: ${DifferingKeys<
ActualT,
ExpectedT
>}`
type NotExactErrorMessage<ActualT, ExpectedT> = ActualT extends Record<string, unknown>
? ExpectedT extends Record<string, unknown>
? `Mismatched property: ${DifferingKeys<ActualT, ExpectedT>}`
: "Expected a non-object type, but received an object"
: ExpectedT extends Record<string, unknown>
? "Expected an object type, but received a non-object type"
: "Types are not exactly equal"

type IsExact<T1, T2> = [T1] extends [T2] ? ([T2] extends [T1] ? Exact<T1, T2> : never) : never

const assertTypesEqual = <ActualT, ExpectedT>(
t: ActualT,
u: Exact<ActualT, ExpectedT> extends never ? InexactErrorMessage<ActualT, ExpectedT> : ExpectedT
u: IsExact<ActualT, ExpectedT> extends never
? NotExactErrorMessage<ActualT, ExpectedT>
: ExpectedT
): [ActualT, ExpectedT] => [t, u] as [ActualT, ExpectedT]
const _: unknown = undefined

Expand Down Expand Up @@ -1172,3 +1179,52 @@ test("maybe / optional type inference verification", () => {
}
)
})

test("union type inference verification for small number of types", () => {
const T = types.union(types.boolean, types.literal("test"), types.maybe(types.number))

type ITC = SnapshotIn<typeof T>
type ITS = SnapshotOut<typeof T>

assertTypesEqual(_ as ITC, _ as boolean | "test" | number | undefined)
assertTypesEqual(_ as ITS, _ as boolean | "test" | number | undefined)
})

test("union type inference verification for a large number of types", () => {
const T = types.union(
types.literal("a"),
types.literal("b"),
types.literal("c"),
types.literal("d"),
types.literal("e"),
types.literal("f"),
types.literal("g"),
types.literal("h"),
types.literal("i"),
types.literal("j")
)

type ITC = SnapshotIn<typeof T>
type ITS = SnapshotOut<typeof T>

assertTypesEqual(_ as ITC, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j")
assertTypesEqual(_ as ITS, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j")
})

test("enumeration type inference verification", () => {
const HexLetter = types.enumeration("HexadecimalLetter", ["a", "b", "c", "d", "e", "f"])

type LetterITC = SnapshotIn<typeof HexLetter>
type LetterITS = SnapshotOut<typeof HexLetter>

assertTypesEqual(_ as LetterITC, _ as "a" | "b" | "c" | "d" | "e" | "f")
assertTypesEqual(_ as LetterITS, _ as "a" | "b" | "c" | "d" | "e" | "f")

const Digit = types.enumeration(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"])

type DigitITC = SnapshotIn<typeof Digit>
type DigitITS = SnapshotOut<typeof Digit>

assertTypesEqual(_ as DigitITC, _ as "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
assertTypesEqual(_ as DigitITS, _ as "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
})
18 changes: 10 additions & 8 deletions src/types/utility-types/enumeration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ export type UnionStringArray<T extends readonly string[]> = T[number]
// these overloads also allow both mutable and immutable arrays, making types.enumeration<Enum>(Object.values(Enum)) possible.
// the only case where this doesn't work is when passing to the function an array variable with a mutable type constraint;
// for these cases, it will just fallback and assume the type is a generic string.
export function enumeration<T extends readonly string[]>(
options: T
): ISimpleType<UnionStringArray<T>>
export function enumeration<T extends string>(
options: readonly T[]
): ISimpleType<UnionStringArray<T[]>>
export function enumeration<T extends string>(
name: string,
options: T[]
options: readonly T[]
): ISimpleType<UnionStringArray<T[]>>

/**
* `types.enumeration` - Can be used to create an string based enumeration.
* (note: this methods is just sugar for a union of string literals)
Expand All @@ -31,8 +30,11 @@ export function enumeration<T extends string>(
* @param options possible values this enumeration can have
* @returns
*/
export function enumeration(name: string | string[], options?: any): ISimpleType<string> {
const realOptions: string[] = typeof name === "string" ? options! : name
export function enumeration<T extends string>(
name: string | readonly T[],
options?: readonly T[]
): ISimpleType<T[number]> {
const realOptions: readonly T[] = typeof name === "string" ? options! : name
// check all options
if (devMode()) {
realOptions.forEach((option, i) => {
Expand All @@ -41,5 +43,5 @@ export function enumeration(name: string | string[], options?: any): ISimpleType
}
const type = union(...realOptions.map((option) => literal("" + option)))
if (typeof name === "string") type.name = name
return type
return type as ISimpleType<T[number]>
}
Loading