Skip to content

Commit

Permalink
Rename fieldsUnion to taggedUnion (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
lydell committed Oct 30, 2023
1 parent 0999785 commit f06963a
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 75 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
Note: I’m currently working on several breaking changes to tiny-decoders, but I’m trying out releasing them piece by piece. The idea is that you can either upgrade version by version only having to deal with one or a few breaking changes at a time, or wait and do a bunch of them at the same time.

### Version 22.0.0 (unreleased)

This release renames `fieldsUnion` to `taggedUnion` since it better describes what it is, and it goes along better with the `tag` function.

### Version 21.0.0 (2023-10-30)

This release renames `nullable` to `nullOr` to be consistent with `undefinedOr`.
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ Here’s a summary of all codecs (with slightly simplified type annotations) and
- Unions:
- Of primitive literals: [primitiveUnion](#primitiveunion)
- Of different types: [multi](#multi)
- Of tagged objects: [fieldsUnion](#fieldsunion) with [tag](#tag)
- Of tagged objects: [taggedUnion](#taggedunion) with [tag](#tag)
- With undefined: [undefinedOr](#undefinedor)
- With null: [nullOr](#nullOr)
- Other unions: [untagged union example](examples/untagged-union.test.ts)
- Intersections: [intersection example](examples/fieldsUnion-with-common-fields.test.ts)
- Intersections: [intersection example](examples/taggedUnion-with-common-fields.test.ts)
- Transformation: [map](#map), [flatMap](#flatmap)
- Recursion: [recursive](#recursive)
- Errors: [DecoderError](#decodererror), [format](#format), [repr](#repr)
Expand Down Expand Up @@ -250,7 +250,7 @@ Here’s a summary of all codecs (with slightly simplified type annotations) and
<td>n/a, only used with <code>fieldsAuto</code></td>
</tr>
<tr>
<th><a href="#fieldsunion">fieldsUnion</a></th>
<th><a href="#taggedunion">taggedUnion</a></th>
<td><pre>(
decodedCommonField: string,
variants: Array&lt;
Expand Down Expand Up @@ -602,10 +602,10 @@ type Example = {
>
> All in all, you avoid a slight gotcha with optional fields and inferred types if you enable `exactOptionalPropertyTypes`.
### fieldsUnion
### taggedUnion

```ts
function fieldsUnion<
function taggedUnion<
const DecodedCommonField extends keyof Variants[number],
Variants extends readonly [
Variant<DecodedCommonField>,
Expand Down Expand Up @@ -646,7 +646,7 @@ type Shape =
| { tag: "Circle"; radius: number }
| { tag: "Rectangle"; width: number; height: number };

const shapeCodec: Codec<Shape> = fieldsUnion("tag", [
const shapeCodec: Codec<Shape> = taggedUnion("tag", [
{
tag: tag("Circle"),
radius: number,
Expand All @@ -664,7 +664,7 @@ The `allowExtraFields` option works just like for [fieldsAuto](#fieldsauto).
See also these examples:

- [Renaming union field](examples/renaming-union-field.test.ts)
- [`fieldsUnion` with common fields](examples/fieldsUnion-with-common-fields.test.ts)
- [`taggedUnion` with common fields](examples/taggedUnion-with-common-fields.test.ts)

Note: If you use the same tag value twice, the last one wins. TypeScript infers a type with two variants with the same tag (which is a valid type), but tiny-decoders can’t tell them apart. Nothing will ever decode to the first one, only the last one will succeed. Trying to encode the first one might result in bad data.

Expand Down Expand Up @@ -693,13 +693,13 @@ function tag<
type primitive = bigint | boolean | number | string | symbol | null | undefined;
```

Used with [fieldsUnion](#fieldsunion), once for each variant of the union.
Used with [taggedUnion](#taggedunion), once for each variant of the union.

`tag("MyTag")` returns a `Field` with a codec that requires the input `"MyTag"` and returns `"MyTag"`. The metadata of the `Field` also advertises that the tag value is `"MyTag"`, which `fieldsUnion` uses to know what to do.
`tag("MyTag")` returns a `Field` with a codec that requires the input `"MyTag"` and returns `"MyTag"`. The metadata of the `Field` also advertises that the tag value is `"MyTag"`, which `taggedUnion` uses to know what to do.

`tag("MyTag", { renameTagFrom: "my_tag" })` returns a `Field` with a codec that requires the input `"my_tag"` but returns `"MyTag"`.

For `renameFieldFrom`, see [fieldsUnion](#fieldsunion).
For `renameFieldFrom`, see [taggedUnion](#taggedunion).

You will typically use string tags for your tagged unions, but other primitive types such as `boolean` and `number` are supported too.

Expand Down Expand Up @@ -942,7 +942,7 @@ type DecoderError = {
got: number;
}
| {
tag: "unknown fieldsUnion tag";
tag: "unknown taggedUnion tag";
knownTags: Array<primitive>;
got: unknown;
}
Expand Down Expand Up @@ -1160,6 +1160,6 @@ function either<T, U>(codec1: Codec<T>, codec2: Codec<U>): Codec<T | U>;
The decoder of this codec would try `codec1.decoder` first. If it fails, go on and try `codec2.decoder`. If that fails, present both errors. I consider this a blunt tool.

- If you want either a string or a number, use [multi](#multi). This let’s you switch between any JSON types.
- For objects that can be decoded in different ways, use [fieldsUnion](#fieldsunion). If that’s not possible, see the [untagged union example](examples/untagged-union.test.ts) for how you can approach the problem.
- For objects that can be decoded in different ways, use [taggedUnion](#taggedunion). If that’s not possible, see the [untagged union example](examples/untagged-union.test.ts) for how you can approach the problem.

The above approaches result in a much simpler [DecoderError](#decodererror) type, and also results in much better error messages, since there’s never a need to present something like “decoding failed in the following 2 ways: …”
4 changes: 2 additions & 2 deletions examples/renaming-union-field.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { expectType, TypeEqual } from "ts-expect";
import { expect, test } from "vitest";

import { fieldsUnion, Infer, InferEncoded, number, tag } from "../";
import { Infer, InferEncoded, number, tag, taggedUnion } from "../";

test("using different tags in JSON and in TypeScript", () => {
// Here’s how to use different keys and values in JSON and TypeScript.
// For example, `"type": "circle"` → `tag: "Circle"`.
const shapeCodec = fieldsUnion("tag", [
const shapeCodec = taggedUnion("tag", [
{
tag: tag("Circle", { renameTagFrom: "circle", renameFieldFrom: "type" }),
radius: number,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { expectType, TypeEqual } from "ts-expect";
import { expect, test } from "vitest";

import { Codec, fieldsUnion, Infer, number, tag } from "../";
import { Codec, Infer, number, tag, taggedUnion } from "../";
import { run } from "../tests/helpers";

test("fieldsUnion with fallback for unknown tags", () => {
test("taggedUnion with fallback for unknown tags", () => {
// Here’s a helper function that takes a codec – which is supposed to be a
// `fieldsUnion` codec – and makes it return `undefined` if the tag is unknown.
// `taggedUnion` codec – and makes it return `undefined` if the tag is unknown.
function handleUnknownTag<Decoded, Encoded>(
codec: Codec<Decoded, Encoded>,
): Codec<Decoded | undefined, Encoded | undefined> {
Expand All @@ -15,8 +15,8 @@ test("fieldsUnion with fallback for unknown tags", () => {
const decoderResult = codec.decoder(value);
switch (decoderResult.tag) {
case "DecoderError":
return decoderResult.error.path.length === 1 && // Don’t match on nested `fieldsUnion`.
decoderResult.error.tag === "unknown fieldsUnion tag"
return decoderResult.error.path.length === 1 && // Don’t match on nested `taggedUnion`.
decoderResult.error.tag === "unknown taggedUnion tag"
? { tag: "Valid", value: undefined }
: decoderResult;
case "Valid":
Expand All @@ -28,12 +28,12 @@ test("fieldsUnion with fallback for unknown tags", () => {
};
}

const shapeCodec = fieldsUnion("tag", [
const shapeCodec = taggedUnion("tag", [
{ tag: tag("Circle"), radius: number },
{ tag: tag("Square"), side: number },
]);

const codec = fieldsUnion("tag", [
const codec = taggedUnion("tag", [
{ tag: tag("One") },
{ tag: tag("Two"), value: shapeCodec },
]);
Expand Down Expand Up @@ -74,7 +74,7 @@ test("fieldsUnion with fallback for unknown tags", () => {
`);
expect(run(codecWithFallback, { tag: "Three" })).toBeUndefined();

// A nested `fieldsUnion` still fails on unknown tags:
// A nested `taggedUnion` still fails on unknown tags:
expect(run(codecWithFallback, { tag: "Two", value: { tag: "Rectangle" } }))
.toMatchInlineSnapshot(`
At root["value"]["tag"]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import {
boolean,
Codec,
fieldsAuto,
fieldsUnion,
Infer,
InferEncoded,
number,
string,
tag,
taggedUnion,
} from "../";
import { run } from "../tests/helpers";

test("fieldsUnion with common fields", () => {
test("taggedUnion with common fields", () => {
// This function takes two codecs for object types and returns
// a new codec which is the intersection of those.
// This function is not part of the tiny-decoders package because it has some caveats:
Expand Down Expand Up @@ -56,7 +56,7 @@ test("fieldsUnion with common fields", () => {
}

type EventWithPayload = Infer<typeof EventWithPayload>;
const EventWithPayload = fieldsUnion("event", [
const EventWithPayload = taggedUnion("event", [
{
event: tag("opened"),
payload: string,
Expand Down
6 changes: 3 additions & 3 deletions examples/untagged-union.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
Codec,
field,
fieldsAuto,
fieldsUnion,
Infer,
InferEncoded,
number,
string,
tag,
taggedUnion,
unknown,
} from "../";
import { run } from "../tests/helpers";
Expand Down Expand Up @@ -119,7 +119,7 @@ test("tagged tuples", () => {
},
};

// A function that takes a regular `fieldsUnion` codec, but makes it work on
// A function that takes a regular `taggedUnion` codec, but makes it work on
// tagged tuples instead.
function toArrayUnion<
Decoded extends Record<string, unknown>,
Expand All @@ -141,7 +141,7 @@ test("tagged tuples", () => {

type Shape = Infer<typeof Shape>;
const Shape = toArrayUnion(
fieldsUnion("tag", [
taggedUnion("tag", [
{
tag: tag("Circle", { renameFieldFrom: "0" }),
radius: field(number, { renameFrom: "1" }),
Expand Down
26 changes: 13 additions & 13 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ type Variant<DecodedCommonField extends number | string | symbol> = Record<
> &
Record<string, Codec<any> | Field<any, any, FieldMeta>>;

export function fieldsUnion<
export function taggedUnion<
const DecodedCommonField extends keyof Variants[number],
Variants extends readonly [
Variant<DecodedCommonField>,
Expand All @@ -440,7 +440,7 @@ export function fieldsUnion<
Variants[number]
> extends never
? [
"fieldsUnion variants must have a field in common, and their encoded field names must be the same",
"taggedUnion variants must have a field in common, and their encoded field names must be the same",
never,
]
: DecodedCommonField,
Expand All @@ -451,7 +451,7 @@ export function fieldsUnion<
InferEncodedFieldsUnion<Variants[number]>
> {
if (decodedCommonField === "__proto__") {
throw new Error("fieldsUnion: decoded common field cannot be __proto__");
throw new Error("taggedUnion: decoded common field cannot be __proto__");
}

type VariantCodec = Codec<any, any>;
Expand All @@ -473,7 +473,7 @@ export function fieldsUnion<
maybeEncodedCommonField = encodedFieldName;
} else if (maybeEncodedCommonField !== encodedFieldName) {
throw new Error(
`fieldsUnion: Variant at index ${index}: Key ${JSON.stringify(
`taggedUnion: Variant at index ${index}: Key ${JSON.stringify(
decodedCommonField,
)}: Got a different encoded field name (${JSON.stringify(
encodedFieldName,
Expand All @@ -487,7 +487,7 @@ export function fieldsUnion<

if (typeof maybeEncodedCommonField !== "string") {
throw new Error(
`fieldsUnion: Got unusable encoded common field: ${repr(
`taggedUnion: Got unusable encoded common field: ${repr(
maybeEncodedCommonField,
)}`,
);
Expand All @@ -509,7 +509,7 @@ export function fieldsUnion<
return {
tag: "DecoderError",
error: {
tag: "unknown fieldsUnion tag",
tag: "unknown taggedUnion tag",
knownTags: Array.from(decoderMap.keys()),
got: encodedName,
path: [encodedCommonField],
Expand All @@ -525,7 +525,7 @@ export function fieldsUnion<
const encoder = encoderMap.get(decodedName);
if (encoder === undefined) {
throw new Error(
`fieldsUnion: Unexpectedly found no encoder for decoded variant name: ${JSON.stringify(
`taggedUnion: Unexpectedly found no encoder for decoded variant name: ${JSON.stringify(
decodedName,
)} at key ${JSON.stringify(decodedCommonField)}`,
);
Expand Down Expand Up @@ -952,11 +952,6 @@ export type DecoderError = {
expected: number;
got: number;
}
| {
tag: "unknown fieldsUnion tag";
knownTags: Array<primitive>;
got: unknown;
}
| {
tag: "unknown multi type";
knownTypes: Array<
Expand All @@ -975,6 +970,11 @@ export type DecoderError = {
knownVariants: Array<primitive>;
got: unknown;
}
| {
tag: "unknown taggedUnion tag";
knownTags: Array<primitive>;
got: unknown;
}
| {
tag: "wrong tag";
expected: primitive;
Expand Down Expand Up @@ -1039,7 +1039,7 @@ function formatDecoderErrorVariant(
: variant.knownTypes.join(", ")
}\nGot: ${formatGot(variant.got)}`;

case "unknown fieldsUnion tag":
case "unknown taggedUnion tag":
return `Expected one of these tags:${primitiveList(
variant.knownTags,
)}\nGot: ${formatGot(variant.got)}`;
Expand Down
Loading

0 comments on commit f06963a

Please sign in to comment.