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

Remove the fields function #34

Merged
merged 1 commit into from
Oct 22, 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
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