Skip to content

Commit

Permalink
Add UnionCake and union (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinyaodu authored Jan 27, 2023
1 parent 07af526 commit 04cfd49
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 22 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,28 @@ const Numbers = array(number);
Numbers.is([2, 3]); // true
```

</td></tr>
<tr><td>

A union:

```ts
type NullableString = string | null;
```

</td><td>

Use [union](#union):

```ts
import { string, union } from "caketype";

const NullableString = union(string, null);

NullableString.is("hello"); // true
NullableString.is(null); // true
```

</td></tr>
</table>

Expand Down Expand Up @@ -337,6 +359,7 @@ Numbers.is([2, 3]); // true
- [`number`](#number)
- [`string`](#string)
- [`symbol`](#symbol)
- [`union`](#union)
- [`unknown`](#unknown)
- [Tags](#tags)
- [`optional`](#optional)
Expand Down Expand Up @@ -790,6 +813,33 @@ symbol.is(Symbol("hi")); // true

---

#### `union`

Return a [Cake](#cake) representing a union of the specified types.

Union members can be existing Cakes:

```ts
// like the TypeScript type 'string | number'
const StringOrNumber = union(string, number);

StringOrNumber.is("hello"); // true
StringOrNumber.is(7); // true
StringOrNumber.is(false); // false
```

Union members can also be primitive values, or any other [Bakeable](#bakeable)s:

```ts
const Color = union("red", "green", "blue");
type Color = Infer<typeof Color>; // "red" | "green" | "blue"

Color.is("red"); // true
Color.is("oops"); // false
```

---

#### `unknown`

A [Cake](#cake) representing the `unknown` type. Every value satisfies this
Expand Down
43 changes: 41 additions & 2 deletions etc/caketype.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,13 @@ export type StringTree = string | readonly [string, readonly StringTree[]];
export const symbol: TypeGuardCake<symbol>;

// Warning: (ae-forgotten-export) The symbol "MapInfer" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "MapInferOptional" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "MapOptional" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "TupleCakeArgs" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export class TupleCake<S extends readonly Cake[], O extends readonly Cake[], R extends Cake | null, E extends readonly Cake[]> extends Cake<[
...MapInfer<S>,
...MapInferOptional<O>,
...MapOptional<MapInfer<O>>,
...(R extends Cake ? [...Infer<R>[]] : []),
...MapInfer<E>
]> implements TupleCakeArgs<S, O, R, E> {
Expand Down Expand Up @@ -646,6 +646,45 @@ export class TypeGuardFailedCakeError extends CakeError {
readonly value: unknown;
}

// Warning: (ae-forgotten-export) The symbol "MapBaked" needs to be exported by the entry point index.d.ts
//
// @public
export function union<M extends readonly [Bakeable, ...Bakeable[]]>(...members: M): UnionCake<MapBaked<M>>;

// Warning: (ae-forgotten-export) The symbol "FoldUnion" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export class UnionCake<M extends readonly Cake[]> extends Cake<FoldUnion<MapInfer<M>>> implements UnionCakeArgs<M> {
constructor(args: UnionCakeArgs<M>);
// (undocumented)
dispatchCheck(value: unknown, context: CakeDispatchCheckContext): CakeError | null;
// (undocumented)
dispatchStringify(context: CakeDispatchStringifyContext): string;
// (undocumented)
readonly members: M;
// (undocumented)
withName(name: string | null): UnionCake<M>;
}

// @public (undocumented)
export interface UnionCakeArgs<M extends readonly Cake[]> extends CakeArgs {
// (undocumented)
members: M;
}

// @public (undocumented)
export class UnionCakeError extends CakeError {
constructor(cake: UnionCake<readonly Cake[]>, value: unknown, errors: Record<string, CakeError>);
// (undocumented)
readonly cake: UnionCake<readonly Cake[]>;
// (undocumented)
dispatchFormat(context: CakeErrorDispatchFormatContext): StringTree;
// (undocumented)
readonly errors: Record<string, CakeError>;
// (undocumented)
readonly value: unknown;
}

// @public
export const unknown: TypeGuardCake<unknown>;

Expand Down
24 changes: 5 additions & 19 deletions src/cake/TupleCake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,16 @@ import {
rest,
RestTag,
StringTree,
MapInfer,
} from "./index-internal";

/**
* @internal
*/
type MapInfer<T extends readonly Cake[]> = T extends readonly []
type MapOptional<T extends readonly unknown[]> = T extends readonly []
? []
: T extends readonly [
infer F extends Cake,
...infer R extends readonly Cake[]
]
? [Infer<F>, ...MapInfer<R>]
: unknown[];

/**
* @internal
*/
type MapInferOptional<T extends readonly Cake[]> = T extends readonly []
? []
: T extends readonly [
infer F extends Cake,
...infer R extends readonly Cake[]
]
? [Infer<F>?, ...MapInferOptional<R>]
: T extends readonly [infer F, ...infer R]
? [F?, ...MapOptional<R>]
: unknown[];

/**
Expand Down Expand Up @@ -79,7 +65,7 @@ class TupleCake<
extends Cake<
[
...MapInfer<S>,
...MapInferOptional<O>,
...MapOptional<MapInfer<O>>,
...(R extends Cake ? [...Infer<R>[]] : []),
...MapInfer<E>
]
Expand Down
139 changes: 139 additions & 0 deletions src/cake/UnionCake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { valuesUnsound } from "../index-internal";

import {
bake,
Bakeable,
Cake,
CakeArgs,
CakeDispatchCheckContext,
CakeDispatchStringifyContext,
CakeError,
CakeErrorDispatchFormatContext,
MapBaked,
MapInfer,
StringTree,
} from "./index-internal";

/**
* @public
*/
interface UnionCakeArgs<M extends readonly Cake[]> extends CakeArgs {
members: M;
}

/**
* @internal
*/
type FoldUnion<T extends readonly unknown[]> = T extends readonly []
? never
: T extends readonly [infer F, ...infer R]
? F | FoldUnion<R>
: T[number];

/**
* @public
*/
class UnionCake<M extends readonly Cake[]>
extends Cake<FoldUnion<MapInfer<M>>>
implements UnionCakeArgs<M>
{
readonly members: M;

constructor(args: UnionCakeArgs<M>) {
super(args);
this.members = args.members;
}

dispatchCheck(
value: unknown,
context: CakeDispatchCheckContext
): CakeError | null {
const { recurse } = context;
const errors: Record<string, CakeError> = {};
for (let i = 0; i < this.members.length; i++) {
const error = recurse(this.members[i], value);
if (error === null) {
return null;
}
errors[i] = error;
}
return new UnionCakeError(this, value, errors);
}

dispatchStringify(context: CakeDispatchStringifyContext): string {
if (this.members.length === 0) {
return "never (empty union)";
}

const { recurse } = context;

if (this.members.length === 1) {
return recurse(this.members[0]);
}

return this.members.map((cake) => `(${recurse(cake)})`).join(" | ");
}

withName(name: string | null): UnionCake<M> {
return new UnionCake({ ...this, name });
}
}

/**
* @public
*/
class UnionCakeError extends CakeError {
constructor(
readonly cake: UnionCake<readonly Cake[]>,
readonly value: unknown,
readonly errors: Record<string, CakeError>
) {
super();
}

dispatchFormat(context: CakeErrorDispatchFormatContext): StringTree {
const { recurse, stringifyCake } = context;

const message = `Value does not satisfy type '${stringifyCake(
this.cake
)}': none of the union member(s) are satisfied.`;

return [message, valuesUnsound(this.errors).map((err) => recurse(err))];
}
}

/**
* Return a {@link Cake} representing a union of the specified types.
*
* @example Union members can be existing Cakes:
*
* ```ts
* // like the TypeScript type 'string | number'
* const StringOrNumber = union(string, number);
*
* StringOrNumber.is("hello"); // true
* StringOrNumber.is(7); // true
* StringOrNumber.is(false); // false
* ```
*
* @example Union members can also be primitive values, or any other {@link Bakeable}s:
*
* ```ts
* const Color = union("red", "green", "blue");
* type Color = Infer<typeof Color>; // "red" | "green" | "blue"
*
* Color.is("red"); // true
* Color.is("oops"); // false
* ```
*
* @public
*/
function union<M extends readonly [Bakeable, ...Bakeable[]]>(
...members: M
): UnionCake<MapBaked<M>> {
return new UnionCake({ members: members.map((b) => bake(b)) }) as UnionCake<
MapBaked<M>
>;
}

export { UnionCake, UnionCakeArgs, UnionCakeError, union };
27 changes: 27 additions & 0 deletions src/cake/helper-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Bakeable, Baked, Cake, Infer } from "./index-internal";

/**
* @internal
*/
type MapBaked<T extends readonly Bakeable[]> = T extends readonly []
? []
: T extends readonly [
infer F extends Bakeable,
...infer R extends readonly Bakeable[]
]
? [Baked<F>, ...MapBaked<R>]
: Cake[];

/**
* @internal
*/
type MapInfer<T extends readonly Cake[]> = T extends readonly []
? []
: T extends readonly [
infer F extends Cake,
...infer R extends readonly Cake[]
]
? [Infer<F>, ...MapInfer<R>]
: unknown[];

export { MapBaked, MapInfer };
3 changes: 3 additions & 0 deletions src/cake/index-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ export * from "./ReferenceCake";
export * from "./StringTree";
export * from "./TupleCake";
export * from "./TypeGuardCake";
export * from "./UnionCake";

export * from "./ArrayCake";

export * from "./helper-types";
5 changes: 5 additions & 0 deletions src/cake/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,9 @@ export {
string,
symbol,
unknown,
// UnionCake.ts
UnionCake,
UnionCakeArgs,
UnionCakeError,
union,
} from "./index-internal";
2 changes: 2 additions & 0 deletions tests/cake/Cake-withName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
reference,
TupleCake,
TypeGuardCake,
union,
} from "../../src";
import { is_boolean } from "../../src/type-guards";

Expand All @@ -21,6 +22,7 @@ const cakes = {
endElements: [],
}),
typeGuard: new TypeGuardCake({ guard: is_boolean }),
union: union(0),
};

test.each(keysUnsound(cakes))("%s.name is null", (cake) => {
Expand Down
Loading

0 comments on commit 04cfd49

Please sign in to comment.