Skip to content

Commit

Permalink
Add more support for primitive values (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
lydell authored Oct 29, 2023
1 parent ba22e26 commit b364a89
Show file tree
Hide file tree
Showing 9 changed files with 714 additions and 224 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
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 20.0.0 (unreleased)

This release adds more support for primitives.

These are the primitive types:

```ts
type primitive = bigint | boolean | number | string | symbol | null | undefined;
```

- `stringUnion` has been renamed to `primitiveUnion` and now works with literals of any primitive type, not just strings. You can now create a codec for a union of numbers, for example.
- `tag` now accepts literals of any primitive type, not just strings. For example, this allows for easily decoding a tagged union where the discriminator is `isAdmin: true` and `isAdmin: false`, or a tagged union where the tags are numbers.
- A `bigint` codec has been added – a codec for `bigint` values. There are now codecs for all primitive types, except:
- `symbol`: I don’t think this is useful. Use `const mySymbol: unique symbol = Symbol(); primitiveUnion([mySymbol])` instead.
- `undefined`: Use `primitiveUnion([undefined])` if needed.
- `null`: Use `primitiveUnion([null])` if needed.
- `multi` now supports `bigint` and `symbol`, covering all primitive types. Additionally, since `multi` is basically the JavaScript `typeof` operator as a codec, it now also supports `function`.
- `repr` now recognizes `bigint` and prints for example `123n` instead of `BigInt`. It already supported symbols (and all other primitive types) since before.
- The `DecoderError` type had slight changes due to the above. If all you do with errors is `format(error)`, then you won’t notice.

In short, all you need to do to upgrade is change `stringUnion` into `primitiveUnion`.

### Version 19.0.0 (2023-10-29)

This release introduces `Codec`:
Expand Down
124 changes: 90 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,26 +141,41 @@ Here’s a summary of all codecs (with slightly simplified type annotations):
<td><code>number</code></td>
</tr>
<tr>
<th><a href="#bigint">bigint</a></th>
<td><code>Codec&lt;bigint&gt;</code></td>
<td>n/a</td>
<td><code>bigint</code></td>
</tr>
<tr>
<th><a href="#string">string</a></th>
<td><code>Codec&lt;string&gt;</code></td>
<td>string</td>
<td><code>string</code></td>
</tr>
<th><a href="#stringunion">stringUnion</a></th>
<th><a href="#primitiveunion">primitiveUnion</a></th>
<td><pre>(variants: [
"string1",
"string2",
"stringN"
"stringN",
1,
2,
true
]) =&gt;
Codec&lt;
"string1"
| "string2"
| "stringN"
| 1
| 2
| true
&gt;</pre></td>
<td>string</td>
<td>string, number, boolean, null</td>
<td><pre>"string1"
| "string2"
| "stringN"</pre></td>
| "stringN"
| 1
| 2
| true</pre></td>
</tr>
<tr>
<th><a href="#array">array</a></th>
Expand Down Expand Up @@ -253,19 +268,22 @@ Here’s a summary of all codecs (with slightly simplified type annotations):
<td><pre>(types: [
"type1",
"type2",
"type7"
"type10"
]) =&gt;
Codec&lt;
{ type: "type1", value: type1 }
| { type: "type2", value: type2 }
| { type: "type7", value: type7 }
| { type: "type10", value: type10 }
&gt;</pre></td>
<td>you decide</td>
<td>A subset of: <pre>{ type: "undefined"; value: undefined }
| { type: "null"; value: null }
| { type: "boolean"; value: boolean }
| { type: "number"; value: number }
| { type: "bigint"; value: bigint }
| { type: "string"; value: string }
| { type: "symbol"; value: symbol }
| { type: "function"; value: Function }
| { type: "array"; value: Array<unknown> }
| { type: "object"; value: Record<string, unknown> }</pre></td>
</tr>
Expand Down Expand Up @@ -343,6 +361,16 @@ const number: Codec<number, number>;

Codec for a JSON number, and a TypeScript `number`.

### bigint

```ts
const bigint: Codec<bigint, bigint>;
```

Codec for a JavaScript `bigint`, and a TypeScript `bigint`.

Note: JSON does not have bigint. You need to serialize them to strings, and then parse them to bigint. This function does _not_ do that for you. It is only useful when you are decoding values that already are JavaScript bigint, but are `unknown` to TypeScript.

### string

```ts
Expand All @@ -351,17 +379,19 @@ const string: Codec<string, string>;

Codec for a JSON string, and a TypeScript `string`.

### stringUnion
### primitiveUnion

```ts
function stringUnion<
const Variants extends readonly [string, ...Array<string>],
function primitiveUnion<
const Variants extends readonly [primitive, ...Array<primitive>],
>(variants: Variants): Codec<Variants[number], Variants[number]>;

type primitive = bigint | boolean | number | string | symbol | null | undefined;
```

Codec for a set of specific JSON strings, and a TypeScript union of those strings.
Codec for a set of specific primitive values, and a TypeScript union of those values.

The `variants` is an array of the strings you want. You must provide at least one variant.
The `variants` is an array of the values you want. You must provide at least one variant. If you provide exactly one variant, you get a codec for a single, constant, exact value (a union with just one variant).

If you have an object and want to use its keys for a string union there’s an example of that in the [type inference example](examples/type-inference.test.ts).

Expand All @@ -370,7 +400,7 @@ Example:
```ts
type Color = "green" | "red";

const colorCodec: Codec<Color> = stringUnion(["green", "red"]);
const colorCodec: Codec<Color> = primitiveUnion(["green", "red"]);
```

### array
Expand Down Expand Up @@ -418,9 +448,11 @@ type Field<Decoded, Encoded, Meta extends FieldMeta> = Meta & {
type FieldMeta = {
renameFrom?: string | undefined;
optional?: boolean | undefined;
tag?: { decoded: string; encoded: string } | undefined;
tag?: { decoded: primitive; encoded: primitive } | undefined;
};

type primitive = bigint | boolean | number | string | symbol | null | undefined;

type InferFields<Mapping extends FieldsMapping> = magic;

type InferEncodedFields<Mapping extends FieldsMapping> = magic;
Expand Down Expand Up @@ -470,8 +502,10 @@ type Field<Decoded, Encoded, Meta extends FieldMeta> = Meta & {
type FieldMeta = {
renameFrom?: string | undefined;
optional?: boolean | undefined;
tag?: { decoded: string; encoded: string } | undefined;
tag?: { decoded: primitive; encoded: primitive } | undefined;
};

type primitive = bigint | boolean | number | string | symbol | null | undefined;
```

This function takes a codec and lets you:
Expand Down Expand Up @@ -570,20 +604,22 @@ function fieldsUnion<
type Variant<DecodedCommonField extends number | string | symbol> = Record<
DecodedCommonField,
Field<any, any, { tag: { decoded: string; encoded: string } }>
Field<any, any, { tag: { decoded: primitive; encoded: primitive } }>
> &
Record<string, Codec<any> | Field<any, any, FieldMeta>>;
type primitive = bigint | boolean | number | string | symbol | null | undefined;
type InferFieldsUnion<MappingsUnion extends FieldsMapping> = magic;
type InferEncodedFieldsUnion<MappingsUnion extends FieldsMapping> = magic;
// See `fieldsAuto` for the definitions of `Field`, `FieldMeta` and `FieldsMapping`.
```
Codec for JSON objects with a common string field (that tells them apart), and a TypeScript tagged union type.
Codec for JSON objects with a common field (that tells them apart), and a TypeScript tagged union type.

The `decodedCommonField` is the name of the common string field.
The `decodedCommonField` is the name of the common field.

`variants` is an array of objects. Those objects are “`fieldsAuto` objects” – they fit when passed to `fieldsAuto` as well. All of those objects must have `decodedCommonField` as a key, and use the [tag](#tag) function on that key.

Expand Down Expand Up @@ -618,15 +654,12 @@ Note: If you use the same tag value twice, the last one wins. TypeScript infers

```ts
function tag<
const Decoded extends string,
const Encoded extends string,
const Decoded extends primitive,
const Encoded extends primitive,
const EncodedFieldName extends string,
>(
decoded: Decoded,
{
renameTagFrom = decoded,
renameFieldFrom,
}: {
options: {
renameTagFrom?: Encoded;
renameFieldFrom?: EncodedFieldName;
} = {},
Expand All @@ -635,9 +668,11 @@ function tag<
Encoded,
{
renameFrom: EncodedFieldName | undefined;
tag: { decoded: string; encoded: string };
tag: { decoded: primitive; encoded: primitive };
}
>;

type primitive = bigint | boolean | number | string | symbol | null | undefined;
```

Used with [fieldsUnion](#fieldsunion), once for each variant of the union.
Expand All @@ -648,6 +683,8 @@ Used with [fieldsUnion](#fieldsunion), once for each variant of the union.

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

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

### tuple

```ts
Expand Down Expand Up @@ -681,11 +718,14 @@ function multi<Types extends readonly [MultiTypeName, ...Array<MultiTypeName>]>(

type MultiTypeName =
| "array"
| "bigint"
| "boolean"
| "function"
| "null"
| "number"
| "object"
| "string"
| "symbol"
| "undefined";

type Multi<Types> = Types extends any
Expand All @@ -697,8 +737,14 @@ type Multi<Types> = Types extends any
? { type: "boolean"; value: boolean }
: Types extends "number"
? { type: "number"; value: number }
: Types extends "bigint"
? { type: "bigint"; value: bigint }
: Types extends "string"
? { type: "string"; value: string }
: Types extends "symbol"
? { type: "symbol"; value: symbol }
: Types extends "function"
? { type: "function"; value: Function }
: Types extends "array"
? { type: "array"; value: Array<unknown> }
: Types extends "object"
Expand All @@ -707,11 +753,18 @@ type Multi<Types> = Types extends any
: never;
```

Codec for multiple JSON types, and a TypeScript tagged union for those types.
Codec for multiple types, and a TypeScript tagged union for those types.

This is useful for supporting stuff that can be either a string or a number, for example. It lets you do a JavaScript `typeof`, basically.

This is useful for supporting stuff that can be either a string or a number, for example.
The type annotation for `multi` is a bit wacky, but it’s not that complicated to use. The `types` parameter is an array of strings – the wanted types. For example, you can say `["string", "number"]`. Then the decoder will give you back either `{ type: "string", value: string }` or `{ type: "number", value: number }`. You can use [map](#map) to map that to some type of choice, or [flatMap](#flatmap) to decode further.

The type annotation for `multi` is a bit wacky, but it’s not that complicated to use. The `types` parameter is an array of strings – the wanted JSON types. For example, you can say `["string", "number"]`. Then the decoder will give you back either `{ type: "string", value: string }` or `{ type: "number", value: number }`. You can use [map](#map) to map that to some type of choice, or [flatMap](#flatmap) to decode further.
The `types` strings are the same as the JavaScript [typeof](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof) returns, with two exceptions:

- `null` is `"null"` instead of `"object"` (because `typeof null === "object"` is a famous mistake).
- `array` is `"array"` instead of `"object"` (because arrays are very common).

If you need to tell other objects apart, write a custom codec.

Example:

Expand Down Expand Up @@ -872,8 +925,8 @@ type DecoderError = {
}
| {
tag: "unknown fieldsUnion tag";
knownTags: Array<string>;
got: string;
knownTags: Array<primitive>;
got: unknown;
}
| {
tag: "unknown multi type";
Expand All @@ -889,21 +942,24 @@ type DecoderError = {
got: unknown;
}
| {
tag: "unknown stringUnion variant";
knownVariants: Array<string>;
got: string;
tag: "unknown primitiveUnion variant";
knownVariants: Array<primitive>;
got: unknown;
}
| {
tag: "wrong tag";
expected: string;
got: string;
expected: primitive;
got: unknown;
}
| { tag: "array"; got: unknown }
| { tag: "bigint"; got: unknown }
| { tag: "boolean"; got: unknown }
| { tag: "number"; got: unknown }
| { tag: "object"; got: unknown }
| { tag: "string"; got: unknown }
);

type primitive = bigint | boolean | number | string | symbol | null | undefined;
```

The error returned by all decoders. It keeps track of where in the JSON the error occurred.
Expand Down
4 changes: 2 additions & 2 deletions examples/decode-constrained.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
DecoderResult,
fieldsAuto,
format,
primitiveUnion,
string,
stringUnion,
} from "../";

test("decode constrained", () => {
Expand All @@ -17,7 +17,7 @@ test("decode constrained", () => {
});

const codec2 = fieldsAuto({
status: stringUnion(["ok", "error"]), // In a second codec, we have a stricter type.
status: primitiveUnion(["ok", "error"]), // In a second codec, we have a stricter type.
});

// `.decoder` of a codec usually accepts `unknown` – you can pass in anything.
Expand Down
8 changes: 4 additions & 4 deletions examples/type-inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
map,
multi,
number,
primitiveUnion,
string,
stringUnion,
} from "..";
import { run } from "../tests/helpers";

Expand Down Expand Up @@ -50,7 +50,7 @@ test("making a type from a codec", () => {
age: number,
active: boolean,
country: field(string, { optional: true }),
type: stringUnion(["user"]),
type: primitiveUnion(["user"]),
});

// Then, let TypeScript infer the `User` type!
Expand Down Expand Up @@ -107,7 +107,7 @@ test("making a type from a codec", () => {
expect({ tag: "Valid", value: user3 }).toMatchObject(userResult);
});

test("making a type from an object and stringUnion", () => {
test("making a type from an object and primitiveUnion", () => {
// Imagine this being the popular `chalk` terminal coloring package.
const chalk = {
hex:
Expand All @@ -129,7 +129,7 @@ test("making a type from an object and stringUnion", () => {

expectType<TypeEqual<Severity, "Critical" | "High" | "Low" | "Medium">>(true);

const severityCodec = stringUnion(SEVERITIES);
const severityCodec = primitiveUnion(SEVERITIES);
expectType<TypeEqual<Severity, Infer<typeof severityCodec>>>(true);
expect(run(severityCodec, "High")).toBe("High");

Expand Down
Loading

0 comments on commit b364a89

Please sign in to comment.