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

Add UnionCake and union #55

Merged
merged 1 commit into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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