Skip to content

Commit

Permalink
Remove the fields function (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
lydell authored Oct 22, 2023
1 parent 9007228 commit ad2b0a1
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 856 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 14.0.0 (unreleased)

This release removes the `fields` function, which was deprecated in version 11.0.0. See the release notes for version 11.0.0 for how to replace `fields` with `fieldsAuto`, `chain` and custom decoders.

### Version 13.0.0 (2023-10-22)

> **Warning**
Expand Down
77 changes: 2 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,6 @@ Here’s a summary of all decoders (with slightly simplified type annotations):
<td><code>Record&lt;string, T&gt;</code></td>
</tr>
<tr>
<th><a href="#fields">fields</a></th>
<td><pre>(callback: Function) =&gt;
Decoder&lt;T&gt;</pre></td>
<td>object</td>
<td><code>T</code></td>
</tr>
<tr>
<th><a href="#fieldsauto">fieldsAuto</a></th>
<td><pre>(mapping: {
field1: Decoder&lt;T1&gt;,
Expand Down Expand Up @@ -374,72 +367,6 @@ The passed `decoder` is for each value of the object.

For example, `record(number)` decodes an object where the keys can be anything and the values are numbers (into `Record<string, number>`).

### fields

> **Warning**
> This function is deprecated. Use [fieldsAuto](#fieldsAuto) instead.
```ts
function fields<T>(
callback: (
field: <U>(key: string, decoder: Decoder<U>) => U,
object: Record<string, unknown>,
) => T,
{
exact = "allow extra",
allow = "object",
}: {
exact?: "allow extra" | "throw";
allow?: "array" | "object";
} = {},
): Decoder<T>;
```

Decodes a JSON object (or array) into any TypeScript you’d like (`T`), usually an object/interface with known fields.

The type annotation is a bit overwhelming, but using `fields` isn’t super complicated. In a callback, you get a `field` function that you use to pluck out stuff from the JSON object. For example:

```ts
type User = {
age: number;
active: boolean;
name: string;
description?: string | undefined;
version: 1;
};

const userDecoder = fields(
(field): User => ({
// Simple field:
age: field("age", number),
// Renaming a field:
active: field("is_active", boolean),
// Combining two fields:
name: `${field("first_name", string)} ${field("last_name", string)}`,
// Optional field:
description: field("description", undefinedOr(string)),
// Hardcoded field:
version: 1,
}),
);

// Plucking a single field out of an object:
const ageDecoder: Decoder<number> = fields((field) => field("age", number));
```

`field("key", decoder)` essentially runs `decoder(obj["key"])` but with better error messages. The nice thing about `field` is that it does _not_ return a new decoder – but the value of that field! This means that you can do for instance `const type: string = field("type", string)` and then use `type` however you want inside your callback.

`object` is passed in case you need to check stuff like `"my-key" in object`.

Also note that you can return any type from the callback, not just objects. If you’d rather have a tuple you could return that – see the [tuples example](examples/tuples.test.ts).

The `exact` option let’s you choose between ignoring extraneous data and making it a hard error.

- `"allow extra"` (default) allows extra properties on the object (or extra indexes on an array).
- `"throw"` throws a `DecoderError` for extra properties.

The `allow` option defaults to only allowing JSON objects. Set it to `"array"` if you are decoding an array.

### fieldsAuto

```ts
Expand Down Expand Up @@ -779,7 +706,7 @@ Returns a new decoder that also accepts `undefined`. Alternatively, supply a `de

Notes:

- Using `undefinedOr` does _not_ make a field in an object optional (except in the deprecated [fields](#fields) function). It only allows the field to be `undefined`. Similarly, using the [field](#field) function to mark a field as optional does not allow setting the field to `undefined`, only omitting it.
- Using `undefinedOr` does _not_ make a field in an object optional. It only allows the field to be `undefined`. Similarly, using the [field](#field) function to mark a field as optional does not allow setting the field to `undefined`, only omitting it.
- JSON does not have `undefined` (only `null`). So `undefinedOr` is more useful when you are decoding something that does not come from JSON. However, even when working with JSON `undefinedOr` still has a use: If you infer types from decoders, using `undefinedOr` on object fields results in `| undefined` for the type of the field, which allows you to assign `undefined` to it which is occasionally useful.

### nullable
Expand Down Expand Up @@ -1082,6 +1009,6 @@ export function either<T, U>(
This decoder would try `decoder1` first. If it fails, go on and try `decoder2`. 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, use [fields](#fields) and look for the field(s) that tell which variant you’ve got. Then run the appropriate decoder for the rest of the object.
- 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.

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: …”
47 changes: 0 additions & 47 deletions examples/readme.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-shadow */ // TODO: Remove this line when removing the `fields` function.
import { expectType, TypeEqual } from "ts-expect";
import { expect, test } from "vitest";

Expand All @@ -8,7 +7,6 @@ import {
Decoder,
DecoderError,
field,
fields,
fieldsAuto,
number,
repr,
Expand Down Expand Up @@ -138,51 +136,6 @@ test("default vs sensitive error messages", () => {
`);
});

test("fields", () => {
type User = {
age: number;
active: boolean;
name: string;
description?: string | undefined;
version: 1;
};

const userDecoder = fields(
(field): User => ({
// Simple field:
age: field("age", number),
// Renaming a field:
active: field("is_active", boolean),
// Combining two fields:
name: `${field("first_name", string)} ${field("last_name", string)}`,
// Optional field:
description: field("description", undefinedOr(string)),
// Hardcoded field:
version: 1,
}),
);

expect(
userDecoder({
age: 30,
is_active: true,
first_name: "John",
last_name: "Doe",
}),
).toStrictEqual({
active: true,
age: 30,
description: undefined,
name: "John Doe",
version: 1,
});

// Plucking a single field out of an object:
const ageDecoder: Decoder<number> = fields((field) => field("age", number));

expect(ageDecoder({ age: 30 })).toBe(30);
});

test("fieldsAuto", () => {
const exampleDecoder = fieldsAuto({
name: field(string, { optional: true }),
Expand Down
96 changes: 24 additions & 72 deletions examples/type-annotations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { expect, test } from "vitest";

import { Decoder, fields, fieldsAuto, number, string } from "../";
import { Decoder, fieldsAuto, number, string } from "../";

test("type annotations", () => {
// First, a small test type and a function that receives it:
Expand All @@ -20,120 +20,72 @@ test("type annotations", () => {
* MISSPELLED PROPERTY
*/

// Here are two decoders for `Person`, but without explicit type annotations.
// Here’s a decoder for `Person`, but without an explicit type annotation.
// TypeScript will infer what they decode into (try hovering `personDecoder1`
// and `personDecoder1Auto` in your editor!), but it won’t know that you
// intended to decode a `Person`. As you can see, I’ve misspelled `age` as `aye`.
const personDecoder1 = fields((field) => ({
name: field("name", string),
aye: field("age", number),
}));
const personDecoder1Auto = fieldsAuto({
// in your editor!), but it won’t know that you intended to decode a `Person`.
// As you can see, I’ve misspelled `age` as `aye`.
const personDecoder1 = fieldsAuto({
name: string,
aye: number,
});
// Since TypeScript has inferred legit decoders above, it marks the following
// two calls as errors (you can’t pass an object with `aye` as a `Person`),
// while the _real_ errors of course are in the decoders themselves.
// Since TypeScript has inferred a legit decoder above, it marks the following
// call as an error (you can’t pass an object with `aye` as a `Person`),
// while the _real_ error of course is in the decoder itself.
// @ts-expect-error Property 'age' is missing in type '{ name: string; aye: number; }' but required in type 'Person'.
greet(personDecoder1(testPerson));
// @ts-expect-error Property 'age' is missing in type '{ name: string; aye: number; }' but required in type 'Person'.
greet(personDecoder1Auto(testPerson));

// The way to make the above type errors more clear is to provide explicit type
// annotations, so that TypeScript knows what you’re trying to do.
const personDecoder2 = fields(
(field): Person => ({
name: field("name", string),
// @ts-expect-error Object literal may only specify known properties, and 'aye' does not exist in type 'Person'.
aye: field("age", number),
}),
);
// The way to make the above type error more clear is to provide an explicit type
// annotation, so that TypeScript knows what you’re trying to do.
// @ts-expect-error Type 'Decoder<{ name: string; aye: number; }, unknown>' is not assignable to type 'Decoder<Person>'.
// Property 'age' is missing in type '{ name: string; aye: number; }' but required in type 'Person'.ts(2322)
const personDecoder2Auto: Decoder<Person> = fieldsAuto({
const personDecoder2: Decoder<Person> = fieldsAuto({
name: string,
aye: number,
});
greet(personDecoder2(testPerson));
greet(personDecoder2Auto(testPerson));

/*
* EXTRA PROPERTY
*/

// TypeScript allows passing extra properties, so without type annotations
// there are no errors:
const personDecoder5 = fields((field) => ({
name: field("name", string),
age: field("age", number),
extra: field("extra", string),
}));
const personDecoder5Auto = fieldsAuto({
const personDecoder3 = fieldsAuto({
name: string,
age: number,
extra: string,
});
// These would ideally complain about the extra property, but they don’t.
greet(personDecoder5(testPerson));
greet(personDecoder5Auto(testPerson));
// This would ideally complain about the extra property, but it doesn’t.
greet(personDecoder3(testPerson));

// Adding `Decoder<Person>` does not seem to help TypeScript find any errors:
const personDecoder6: Decoder<Person> = fields((field) => ({
name: field("name", string),
age: field("age", number),
extra: field("extra", string),
}));
const personDecoder6Auto: Decoder<Person> = fieldsAuto({
const personDecoder4: Decoder<Person> = fieldsAuto({
name: string,
age: number,
extra: string,
});
greet(personDecoder6(testPerson));
greet(personDecoder6Auto(testPerson));
greet(personDecoder4(testPerson));

// The recommended type annotation for `fields` does produce errors!
const personDecoder7 = fields(
(field): Person => ({
name: field("name", string),
age: field("age", number),
// @ts-expect-error Object literal may only specify known properties, and 'extra' does not exist in type 'Person'.
extra: field("extra", string),
}),
);
const personDecoder7Auto: Decoder<Person> = fieldsAuto({
// This is currently not an error unfortunately, but in a future version of tiny-decoders it will be.
const personDecoder5: Decoder<Person> = fieldsAuto({
name: string,
age: number,
// This is currently not an error unfortunately, but in a future version of tiny-decoders it will be.
// Here is where the error will be.
extra: string,
});
greet(personDecoder7(testPerson));
greet(personDecoder7Auto(testPerson));
greet(personDecoder5(testPerson));
// See these TypeScript issues for more information:
// https://github.com/microsoft/TypeScript/issues/7547
// https://github.com/microsoft/TypeScript/issues/18020

// Finally, some compiling decoders.
const personDecoder8 = fields(
(field): Person => ({
name: field("name", string),
age: field("age", number),
}),
);
const personDecoder8Auto: Decoder<Person> = fieldsAuto({
// Finally, a compiling decoder.
const personDecoder6: Decoder<Person> = fieldsAuto({
name: string,
age: number,
});
greet(personDecoder8(testPerson));
greet(personDecoder8Auto(testPerson));
greet(personDecoder6(testPerson));

expect(personDecoder8(testPerson)).toMatchInlineSnapshot(`
{
"age": 30,
"name": "John",
}
`);
expect(personDecoder8Auto(testPerson)).toMatchInlineSnapshot(`
expect(personDecoder6(testPerson)).toMatchInlineSnapshot(`
{
"age": 30,
"name": "John",
Expand Down
Loading

0 comments on commit ad2b0a1

Please sign in to comment.