tiny-decoders
Advanced tools
Changelog
Version 15.1.0 (2023-10-23)
This release adds the Infer
utility type. It’s currently basically just an alias to the TypeScript built-in ReturnType
utility type, but in a future version of tiny-decoders it’ll need to do a little bit more than just ReturnType
. If you’d like to reduce the amount of migration work when upgrading to that future version, change all your ReturnType<typeof myDecoder>
to Infer<typeof myDecoder>
now!
Changelog
Version 15.0.0 (2023-10-23)
This release changes the options parameter of fieldsAuto
and fieldsUnion
from:
{ exact = "allow extra" }: { exact?: "allow extra" | "throw" } = {}
To:
{ allowExtraFields = true }: { allowExtraFields?: boolean } = {}
This is because:
"throw"
will not make sense anymore.Changelog
Version 14.0.0 (2023-10-22)
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.
Changelog
Version 13.0.0 (2023-10-22)
[!WARNING]
This release contains a breaking change, but no TypeScript errors! Be careful!
Version 11.0.0 made changes to fieldsAuto
, but had a temporary behavior for backwards compatibility, awaiting the changes to fieldsUnion
in version 12.0.0. This release (13.0.0) removes that temporary behavior.
You need to be on the lookout for these two patterns:
fieldsAuto({
field1: undefinedOr(someDecoder),
field2: () => someValue,
});
Previously, the above decoder would succeed even if field1
or field2
were missing.
field1
was missing, the temporary behavior in fieldsAuto
would call the decoder at field1
with undefined
, which would succeed due to undefinedOr
. If you did the version 11.0.0 migration perfectly, this shouldn’t matter. But upgrading to 13.0.0 might uncover some places where you use undefinedOr(someDecoder)
but meant to use field(someDecoder, { optional(true) })
or field(undefinedOr(someDecoder), { optional(true) })
(the latter is the “safest” approach in that it is the most permissive).field2
was missing, the temporary behavior in fieldsAuto
would call the decoder at field2
with undefined
, which would succeed due to that decoder ignoring its input and always succeeding with the same value.Here’s an example of how to upgrade the “always succeed” pattern:
const productDecoder: Decoder<Product> = fieldsAuto({
name: string,
price: number,
version: () => 1,
});
Use chain
instead:
const productDecoder: Decoder<Product> = chain(
fieldsAuto({
name: string,
price: number,
}),
(props) => ({ ...props, version: 1 }),
);
It’s a little bit more verbose, but unlocks further changes that will come in future releases.
Changelog
Version 12.0.0 (2023-10-22)
This release changes how fieldsUnion
works. The new way should be easier to use, and it looks more similar to the type definition of a tagged union.
Changed: The first argument to fieldsUnion
is no longer the common field name used in the JSON, but the common field name used in TypeScript. This doesn’t matter if you use the same common field name in both JSON and TypeScript. But if you did use different names – don’t worry, you’ll get TypeScript errors so you won’t forget to update something.
Changed: The second argument to fieldsUnion
is now an array of objects, instead of an object with decoders. The objects in the array are “fieldsAuto
objects” – they fit when passed to fieldsAuto
as well. All of those objects must have the first argument to fieldsUnion
as a key, and use the new tag
function on that key.
Added: The tag
function. Used with fieldsUnion
, once for each variant of the union. tag("MyTag")
returns a Field
with a decoder 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. The tag
function also lets you use a different common field in JSON than in TypeScript (similar to the field
function for other fields).
Here’s an example of how to upgrade:
fieldsUnion("tag", {
Circle: fieldsAuto({
tag: () => "Circle" as const,
radius: number,
}),
Rectangle: fields((field) => ({
tag: "Rectangle" as const,
width: field("width_px", number),
height: field("height_px", number),
})),
});
After:
fieldsUnion("tag", [
{
tag: tag("Circle"),
radius: number,
},
{
tag: tag("Rectangle"),
width: field(number, { renameFrom: "width_px" }),
height: field(number, { renameFrom: "height_px" }),
},
]);
And here’s an example of how to upgrade a case where the JSON and TypeScript names are different:
fieldsUnion("type", {
circle: fieldsAuto({
tag: () => "Circle" as const,
radius: number,
}),
square: fieldsAuto({
tag: () => "Square" as const,
size: number,
}),
});
After:
fieldsUnion("tag", [
{
tag: tag("Circle", { renameTagFrom: "circle", renameFieldFrom: "type" }),
radius: number,
},
{
tag: tag("Square", { renameTagFrom: "square", renameFieldFrom: "type" }),
size: number,
},
]);
Changelog
Version 11.0.0 (2023-10-21)
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 and #24.
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.
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.
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).
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.
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.
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.)
Changed: TypeScript 5+ is now required, because the above uses const type parameters) (added in 5.0), and leads to the exactOptionalPropertyTypes (added in 4.4) option in tsconfig.json
being recommended (see the documentation for the field
function for why).
The motivation for the changes are:
Supporting TypeScript’s exactOptionalPropertyTypes option. That option decouples optional fields (field?:
) and union with undefined (| undefined
). Now tiny-decoders has done that too.
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
Stop setting all optional fields to undefined
when they are missing (rather than leaving them out). #22
Better error messages for missing required fields.
Before:
At root["firstName"]:
Expected a string
Got: undefined
After:
At root:
Expected an object with a field called: "firstName"
Got: {
"id": 1,
"first_name": "John"
}
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.
[!WARNING]
Temporary behavior: If a field is missing and not marked as optional,fieldsAuto
still tries the decoder at the field (passingundefined
to it). If the decoder succeeds (because it allowsundefined
or succeeds for any input), that value is used. If it fails, the regular “missing field” error is thrown. This means thatfieldsAuto({ 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 howfieldsUnion
is used currently. WhenfieldsUnion
is updated to a new API in an upcoming version of tiny-decoders, this temporary behavior infieldsAuto
will be removed.
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.
Getting rid of fields
unlocks further changes that will come in future releases. (Note: fields
is only deprecated in this release, not removed.)
Here’s an example illustrating the difference between optional fields and accepting undefined
:
fieldsAuto({
// Required field.
a: string,
// Optional field.
b: field(string, { optional: true }),
// Required field that can be set to `undefined`:
c: undefinedOr(string),
// Optional field that can be set to `undefined`:
d: field(undefinedOr(string), { optional: true }),
});
The inferred type of the above is:
type Inferred = {
a: string;
b?: string;
c: string | undefined;
d?: string | undefined;
};
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 })
.
The field
function also lets you rename fields. This means that you can refactor:
fields((field) => ({
firstName: field("first_name", string),
}));
Into:
fieldsAuto({
firstName: field(string, { renameFrom: "first_name" }),
});
If you used fields
for other reasons, you can refactor them away by using recursive
, chain
and writing custom decoders.
Read the documentation for fieldsAuto
and field
to learn more about how they work.
Changelog
Version 10.0.0 (2023-10-15)
Changed: multi
has a new API.
Before:
type Id = { tag: "Id"; id: string } | { tag: "LegacyId"; id: number };
const idDecoder: Decoder<Id> = multi({
string: (id) => ({ tag: "Id" as const, id }),
number: (id) => ({ tag: "LegacyId" as const, id }),
});
After:
type Id = { tag: "Id"; id: string } | { tag: "LegacyId"; id: number };
const idDecoder: Decoder<Id> = chain(multi(["string", "number"]), (value) => {
switch (value.type) {
case "string":
return { tag: "Id" as const, id: value.value };
case "number":
return { tag: "LegacyId" as const, id: value.value };
}
});
Like before, you specify the types you want (string
and number
above), but now you get a tagged union back ({ type: "string", value: string } | { type: "number", value: number }
) instead of supplying functions to call for each type. You typically want to pair this with chain
, switching on the different variants of the tagged union.
This change unlocks further changes that will come in future releases.
Changelog
Version 9.0.0 (2023-10-15)
Changed: repr
now prints objects and arrays slightly differently, and some options have changed.
tiny-decoders has always printed representations of values on a single line. This stems back to when tiny-decoders used to print a “stack trace” (showing you a little of each parent object and array) – then it was useful to have a very short, one-line representation. Since that’s not a thing anymore, it’s more helpful to print objects and arrays multi-line: One array item or object key–value per line.
Here’s how the options have changed:
recurse: boolean
: Replaced by depth: number
. Defaults to 0 (which prints the current object or array, but does not recurse).recurseMaxLength
: Removed. maxLength
is now used always. This is because values are printed multi-line; apart for the indentation there’s the same amount of space available regardless of how deeply nested a value is.maxObjectChildren
: The default has changed from 3 to 5, which is the same as for maxArrayChildren
.indent: string
option is the indent used when recursing. It defaults to " "
(two spaces).Before:
At root["user"]:
Expected a string
Got: {"firstName": "John", "lastName": "Doe", "dateOfBirth": Date, (4 more)}
After:
At root["user"]:
Expected a string
Got: {
"firstName": "John",
"lastName": "Doe",
"dateOfBirth": Date,
"tags": Array(2),
"likes": 42,
(2 more)
}
Changelog
Version 8.0.0 (2023-10-14)
Changed: stringUnion
now takes an array instead of an object.
Before:
stringUnion({ green: null, red: null });
After:
stringUnion(["green", "red"]);
This is clearer, and made the implementation of stringUnion
simpler.
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 file.