Skip to content

Commit db5f2cb

Browse files
committed
Improve changelog and documentation
1 parent 053227e commit db5f2cb

File tree

3 files changed

+140
-16
lines changed

3 files changed

+140
-16
lines changed

CHANGELOG.md

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,62 @@ Note: I’m currently working on several breaking changes to tiny-decoders, but
22

33
### Version 11.0.0 (unreleased)
44

5-
- Changed: `fieldsAuto` works slightly differently. As part of that change, `optional` has been removed and replaced by `undefinedOr` and `field`. These changes make `fields` redundant, so it has been deprecated.
5+
This release deprecates `fields`, and makes `fieldsAuto` more powerful so that it can do most of what only `fields` could before. Removing `fields` unlocks further changes that will come in future releases. It’s also nice to have just one way of decoding objects (`fieldsAuto`), instead of having two. Finally, the changes to `fieldsAuto` gets rid of a flawed design choice which solves several reported bugs: [#22](https://github.com/lydell/tiny-decoders/issues/22) and [#24](https://github.com/lydell/tiny-decoders/issues/24).
66

7-
- Added: `recursive`. It’s needed now that `fields` has been deprecated.
7+
- Changed: `optional` has been removed and replaced by `undefinedOr` and a new function called `field`. The `optional` function did two things: It made a decoder also accept `undefined`, and marked fields as optional. Now there’s one function for each use case.
88

9-
- Changed: TypeScript 5+, because the above uses [const type parameters](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#const-type-parameters)) (added in 5.0), and leads to [exactOptionalPropertyTypes](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) (added in 4.4) option in `tsconfig.json` being recommended.
9+
- Added: The new `field` function returns a `Field` type, which is a decoder with some metadata. The metadata tells whether the field is optional, and whether the field has a different name in the JSON object.
10+
11+
- Changed: `fieldsAuto` takes an object like before, where the values are `Decoder`s like before, but now the values can be `Field`s as well (returned from the `field` function). Passing a plain `Decoder` instead of a `Field` is just a convenience shortcut for passing a `Field` with the default metadata (the field is required, and has the same name both in TypeScript and in JSON).
12+
13+
- Changed: `fieldsAuto` no longer computes which fields are optional by checking if the type of the field includes `| undefined`. Instead, it’s based purely on the `Field` metadata.
14+
15+
- Changed: `const myDecoder = fieldsAuto<MyType>({ /* ... */ })` now needs to be written as `const myDecoder: Decoder<MyType> = fieldsAuto({ /* ... */ })`. It is no longer recommended to specify the generic of `fieldsAuto`, and doing so does not mean the same thing anymore. Either annotate the decoder as any other, or don’t and infer the type.
16+
17+
- Added: `recursive`. It’s needed when making a decoder for a recursive data structure using `fieldsAuto`. (Previously, the recommendation was to use `fields` for recursive objects.)
18+
19+
- Changed: TypeScript 5+ is now required, because the above uses [const type parameters](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#const-type-parameters)) (added in 5.0), and leads to the [exactOptionalPropertyTypes](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) (added in 4.4) option in `tsconfig.json` being recommended (see the documentation for the `field` function for why).
1020

1121
The motivation for the changes are:
1222

13-
- Supporting TypeScript’s [exactOptionalPropertyTypes](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/#exact-optional-property-types) option.
14-
- Supporting generic decoders.
15-
- Stop setting all optional fields to `undefined` when they are missing (rather than leaving them out).
23+
- Supporting TypeScript’s [exactOptionalPropertyTypes](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/#exact-optional-property-types) option. That option decouples optional fields (`field?:`) and union with undefined (`| undefined`). Now tiny-decoders has done that too.
24+
25+
- Supporting generic decoders. Marking the fields as optional was previously done by looking for fields with `| undefined` in their type. However, if the type of a field is generic, TypeScript can’t know if the type is going to have `| undefined` until the generic type is instantiated with a concrete type. As such it couldn’t know if the field should be optional or not yet either. This resulted in it being very difficult and ugly trying to write a type annotation for a generic function returning a decoder – in practice it was unusable without forcing TypeScript to the wanted type annotation. [#24](https://github.com/lydell/tiny-decoders/issues/24)
26+
27+
- Stop setting all optional fields to `undefined` when they are missing (rather than leaving them out). [#22](https://github.com/lydell/tiny-decoders/issues/22)
28+
1629
- Better error messages for missing required fields.
17-
- Being able to rename fields with `fieldsAuto`. Now you don’t need to refactor from `fieldsAuto` to `fields` anymore if you need to rename a field.
18-
- Getting rid of `fields` unlocks further changes that will come in future releases. (Note: `fields` is only deprecated in this release, not removed.)
1930

20-
The `optional` function did two things: It made a decoder also accept `undefined`, and marked fields as optional. Marking the fields as optional was done by looking for fields with `| undefined` in their type. However, if the type of a field is generic, TypeScript can’t know if the type is going to have `| undefined` until the generic type is instantiated with a concrete type. As such it couldn’t know if the field should be optional or not yet either. This resulted in it being very difficult and ugly trying to write a type annotation for a generic function returning a decoder. This design also doesn’t let you use `exactOptionalPropertyTypes` as intended. The whole concept was flawed.
31+
Before:
2132

22-
`optional` has therefore been renamed to `undefinedOr`, and now it does only one thing: Makes a decoder also accept `undefined`.
33+
```
34+
At root["firstName"]:
35+
Expected a string
2336
24-
The `field` function has been introduced to mark fields as optional, separating the two jobs that `optional` used to do.
37+
Got: undefined
38+
```
2539

26-
For example:
40+
After:
41+
42+
```
43+
At root:
44+
Expected an object with a field called: "firstName"
45+
Got: {
46+
"id": 1,
47+
"first_name": "John"
48+
}
49+
```
50+
51+
In other words, `fieldsAuto` now checks if fields exist, rather than trying to access them regardless. Previously, `fieldsAuto` ran `decoderAtKey(object[key])` even when `key` did not exist in `object`, which is equivalent to `decoderAtKey(undefined)`. Whether or not that succeeded was up to if `decoderAtKey` was using `optional` or not. This resulted in the worse (but technically correct) error message. The new version of `fieldsAuto` knows if the field is supposed to be optional or not thanks to the `Field` type and the `field` function mentioned above.
52+
53+
> **Warning**
54+
> Temporary behavior: If a field is missing and _not_ marked as optional, `fieldsAuto` still _tries_ the decoder at the field (passing `undefined` to it). If the decoder succeeds (because it allows `undefined` or succeeds for any input), that value is used. If it fails, the regular “missing field” error is thrown. This means that `fieldsAuto({ name: undefinedOr(string) })` successfully produces `{ name: undefined }` if given `{}` as input. It is supposed to fail in that case (because a required field is missing), but temporarily it does not fail. This is to support how `fieldsUnion` is used currently. When `fieldsUnion` is updated to a new API in an upcoming version of tiny-decoders, this temporary behavior in `fieldsAuto` will be removed.
55+
56+
- Being able to rename fields with `fieldsAuto`. Now you don’t need to refactor from `fieldsAuto` to `fields` anymore if you need to rename a field. This is done by using the `field` function.
57+
58+
- Getting rid of `fields` unlocks further changes that will come in future releases. (Note: `fields` is only deprecated in this release, not removed.)
59+
60+
Here’s an example illustrating the difference between optional fields and accepting `undefined`:
2761

2862
```ts
2963
fieldsAuto({
@@ -41,6 +75,17 @@ fieldsAuto({
4175
});
4276
```
4377

78+
The inferred type of the above is:
79+
80+
```ts
81+
type Inferred = {
82+
a: string;
83+
b?: string;
84+
c: string | undefined;
85+
d?: string | undefined;
86+
};
87+
```
88+
4489
In all places where you use `optional(x)` currently, you need to figure out if you should use `undefinedOr(x)` or `field(x, { optional: true })` or `field(undefinedOr(x), { optional: true })`.
4590

4691
The `field` function also lets you rename fields. This means that you can refactor:

README.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ npm install tiny-decoders
1717

1818
tiny-decoders requires TypeScript 5+ (because it uses [const type parameters](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#const-type-parameters)).
1919

20-
It is recommended to enable the [exactOptionalPropertyTypes](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) option in `tsconfig.json`.
20+
It is recommended to enable the [exactOptionalPropertyTypes](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) option in `tsconfig.json` – see the note at the [field](#field) function.
2121

2222
Note that it is possible to use tiny-decoders in plain JavaScript without type checking as well.
2323

@@ -79,7 +79,7 @@ You can even [infer the type from the decoder](#type-inference) instead of writi
7979
type User2 = ReturnType<typeof userDecoder2>;
8080
```
8181

82-
The above produces the `User` type already shown above.
82+
`User2` above is equivalent to the `User` type already shown earlier.
8383

8484
## Decoder&lt;T&gt;
8585

@@ -494,6 +494,10 @@ function field<Decoded, const Meta extends FieldMeta>(
494494
meta: Meta,
495495
): Field<Decoded, Meta>;
496496

497+
type Field<Decoded, Meta extends FieldMeta> = Meta & {
498+
decoder: Decoder<Decoded>;
499+
};
500+
497501
type FieldMeta = {
498502
renameFrom?: string | undefined;
499503
optional?: boolean | undefined;
@@ -508,6 +512,35 @@ This function takes a decoder and lets you:
508512

509513
Use it with [fieldsAuto](#fieldsAuto).
510514

515+
Here’s an example illustrating the difference between `field(string, { optional: true })` and `undefinedOr(string)`:
516+
517+
```ts
518+
const exampleDecoder = fieldsAuto({
519+
// Required field.
520+
a: string,
521+
522+
// Optional field.
523+
b: field(string, { optional: true }),
524+
525+
// Required field that can be set to `undefined`:
526+
c: undefinedOr(string),
527+
528+
// Optional field that can be set to `undefined`:
529+
d: field(undefinedOr(string), { optional: true }),
530+
});
531+
```
532+
533+
The inferred type of `exampleDecoder` is:
534+
535+
```ts
536+
type Example = {
537+
a: string;
538+
b?: string;
539+
c: string | undefined;
540+
d?: string | undefined;
541+
};
542+
```
543+
511544
> **Warning**
512545
> It is recommended to enable the [exactOptionalPropertyTypes](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) option in `tsconfig.json`.
513546
>
@@ -543,7 +576,7 @@ Use it with [fieldsAuto](#fieldsAuto).
543576
>
544577
> That gives the same inferred type, but also supports decoding the `name` field being set to `undefined` explicitly.
545578
>
546-
> All in all, you avoid a slight gotcha with optional fields and inferred types if you use `exactOptionalPropertyTypes`.
579+
> All in all, you avoid a slight gotcha with optional fields and inferred types if you enable `exactOptionalPropertyTypes`.
547580
548581
### fieldsUnion
549582
@@ -945,10 +978,15 @@ Rather than first defining the type and then defining the decoder (which often f
945978
```ts
946979
const personDecoder = fieldsAuto({
947980
name: string,
948-
age: optional(number),
981+
age: number,
949982
});
950983

951984
type Person = ReturnType<typeof personDecoder>;
985+
// equivalent to:
986+
type Person = {
987+
name: string;
988+
age: number;
989+
};
952990
```
953991

954992
See the [type inference example](examples/type-inference.test.ts) for more details.

examples/readme.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,44 @@ test("fieldsAuto", () => {
209209
name: undefined,
210210
});
211211
});
212+
213+
test("field", () => {
214+
const exampleDecoder = fieldsAuto({
215+
// Required field.
216+
a: string,
217+
218+
// Optional field.
219+
b: field(string, { optional: true }),
220+
221+
// Required field that can be set to `undefined`:
222+
c: undefinedOr(string),
223+
224+
// Optional field that can be set to `undefined`:
225+
d: field(undefinedOr(string), { optional: true }),
226+
});
227+
228+
type Example = {
229+
a: string;
230+
b?: string;
231+
c: string | undefined;
232+
d?: string | undefined;
233+
};
234+
235+
expectType<TypeEqual<ReturnType<typeof exampleDecoder>, Example>>(true);
236+
237+
expect(exampleDecoder({ a: "", c: undefined })).toStrictEqual({
238+
a: "",
239+
c: undefined,
240+
});
241+
242+
expect(
243+
exampleDecoder({ a: "", b: "", c: undefined, d: undefined }),
244+
).toStrictEqual({ a: "", b: "", c: undefined, d: undefined });
245+
246+
expect(exampleDecoder({ a: "", b: "", c: "", d: "" })).toStrictEqual({
247+
a: "",
248+
b: "",
249+
c: "",
250+
d: "",
251+
});
252+
});

0 commit comments

Comments
 (0)