diff --git a/CHANGELOG.md b/CHANGELOG.md index df10ac8..e8d68cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 15.1.0 (unreleased) + +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` to `Infer` now! + ### Version 15.0.0 (2023-10-23) This release changes the options parameter of `fieldsAuto` and `fieldsUnion` from: diff --git a/README.md b/README.md index 8d2f650..110b0ea 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ import { DecoderError, field, fieldsAuto, + type Infer, number, string, } from "tiny-decoders"; @@ -76,7 +77,7 @@ Got: "30" You can even [infer the type from the decoder](#type-inference) instead of writing it manually! ```ts -type User2 = ReturnType; +type User2 = Infer; ``` `User2` above is equivalent to the `User` type already shown earlier. @@ -963,7 +964,7 @@ const personDecoder = fieldsAuto({ age: number, }); -type Person = ReturnType; +type Person = Infer; // equivalent to: type Person = { name: string; @@ -971,6 +972,8 @@ type Person = { }; ``` +The `Infer` utility type is currently basically just an alias to the TypeScript built-in `ReturnType` utility type, but it’s recommended to use `Infer` because in a future version of tiny-decoders it’ll need to do a little bit more than just `ReturnType` and then you don’t need to migrate. + See the [type inference example](examples/type-inference.test.ts) for more details. ## Things left out diff --git a/examples/readme.test.ts b/examples/readme.test.ts index 73e38af..b722b87 100644 --- a/examples/readme.test.ts +++ b/examples/readme.test.ts @@ -8,6 +8,7 @@ import { DecoderError, field, fieldsAuto, + Infer, number, repr, ReprOptions, @@ -143,7 +144,7 @@ test("fieldsAuto", () => { type Example = { name?: string }; - expectType, Example>>(true); + expectType, Example>>(true); const exampleDecoder2 = fieldsAuto({ name: field(undefinedOr(string), { optional: true }), @@ -156,7 +157,7 @@ test("fieldsAuto", () => { type Example2 = { name?: string | undefined }; - expectType, Example2>>(true); + expectType, Example2>>(true); expect(exampleDecoder2({ name: undefined })).toStrictEqual({ name: undefined, @@ -185,7 +186,7 @@ test("field", () => { d?: string | undefined; }; - expectType, Example>>(true); + expectType, Example>>(true); expect(exampleDecoder({ a: "", c: undefined })).toStrictEqual({ a: "", diff --git a/examples/renaming-union-field.test.ts b/examples/renaming-union-field.test.ts index 5d65ac5..7da4ba9 100644 --- a/examples/renaming-union-field.test.ts +++ b/examples/renaming-union-field.test.ts @@ -1,7 +1,7 @@ import { expectType, TypeEqual } from "ts-expect"; import { expect, test } from "vitest"; -import { fieldsUnion, number, tag } from "../"; +import { fieldsUnion, Infer, number, tag } from "../"; test("using different tags in JSON and in TypeScript", () => { // Here’s how to use different keys and values in JSON and TypeScript. @@ -17,7 +17,7 @@ test("using different tags in JSON and in TypeScript", () => { }, ]); - type InferredType = ReturnType; + type InferredType = Infer; type ExpectedType = | { tag: "Circle"; radius: number } | { tag: "Square"; size: number }; diff --git a/examples/type-inference.test.ts b/examples/type-inference.test.ts index 169f7d3..cd68cb1 100644 --- a/examples/type-inference.test.ts +++ b/examples/type-inference.test.ts @@ -8,6 +8,7 @@ import { chain, field, fieldsAuto, + Infer, multi, number, string, @@ -18,21 +19,21 @@ test("making a type from a decoder", () => { // Rather than first typing out a `type` for `Person` and then essentially // typing the same thing again in the decoder (especially `fieldsAuto` decoders // look almost identical to `type` they decode to!), you can start with the - // decoder and extract the type afterwards with TypeScript’s `ReturnType` utility. + // decoder and extract the type afterwards with tiny-decoder’s `Infer` utility. const personDecoder = fieldsAuto({ name: string, age: number, }); // Hover over `Person` to see what it looks like! - type Person = ReturnType; + type Person = Infer; expectType>(true); // If it feels like you are specifying everything twice – once in a `type` or - // `interface`, and once in the decoder – you might find this `ReturnType` - // technique interesting. But this `ReturnType` approach you don’t have to + // `interface`, and once in the decoder – you might find this `Infer` + // technique interesting. But this `Infer` approach you don’t have to // write what your records look like “twice.” Personally I don’t always mind - // the “duplication,” but when you do – try out the `ReturnType` approach! + // the “duplication,” but when you do – try out the `Infer` approach! // Here’s a more complex example for trying out TypeScript’s inference. const userDecoder = fieldsAuto({ @@ -45,7 +46,7 @@ test("making a type from a decoder", () => { }); // Then, let TypeScript infer the `User` type! - type User = ReturnType; + type User = Infer; // Try hovering over `User` in the line above – your editor should reveal the // exact shape of the type. @@ -116,7 +117,7 @@ test("making a type from an object and stringUnion", () => { expectType>(true); const severityDecoder = stringUnion(SEVERITIES); - expectType>>(true); + expectType>>(true); expect(severityDecoder("High")).toBe("High"); function coloredSeverity(severity: Severity): string { diff --git a/examples/untagged-union.test.ts b/examples/untagged-union.test.ts index 7e5a1b8..1a0bd48 100644 --- a/examples/untagged-union.test.ts +++ b/examples/untagged-union.test.ts @@ -6,6 +6,7 @@ import { chain, Decoder, fieldsAuto, + Infer, number, string, undefinedOr, @@ -85,9 +86,7 @@ test("tagged union, but using boolean instead of string", () => { }), ); - type User = - | ReturnType - | ReturnType; + type User = Infer | Infer; const userDecoder: Decoder = (value) => { const { isAdmin } = fieldsAuto({ isAdmin: boolean })(value); diff --git a/index.ts b/index.ts index e019be4..efa4385 100644 --- a/index.ts +++ b/index.ts @@ -4,6 +4,8 @@ export type Decoder = (value: U) => T; +export type Infer> = ReturnType; + // Make VSCode show `{ a: string; b?: number }` instead of `{ a: string } & { b?: number }`. // https://stackoverflow.com/a/57683652/2010616 type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; @@ -109,9 +111,9 @@ type FieldsMapping = Record | Field>; type InferField | Field> = T extends Field - ? ReturnType + ? Infer : T extends Decoder - ? ReturnType + ? Infer : never; type InferFields = Expand< diff --git a/tests/decoders.test.ts b/tests/decoders.test.ts index c64e0bd..0f7c6ff 100644 --- a/tests/decoders.test.ts +++ b/tests/decoders.test.ts @@ -91,7 +91,7 @@ test("string", () => { describe("stringUnion", () => { test("basic", () => { - type Color = ReturnType; + type Color = Infer; const colorDecoder = stringUnion(["red", "green", "blue"]); expectType>(true); @@ -136,14 +136,14 @@ describe("stringUnion", () => { // @ts-expect-error Type 'number' is not assignable to type 'string'. stringUnion([1]); const goodDecoder = stringUnion(["1"]); - expectType, "1">>(true); + expectType, "1">>(true); expect(goodDecoder("1")).toBe("1"); }); }); describe("array", () => { test("basic", () => { - type Bits = ReturnType; + type Bits = Infer; const bitsDecoder = array(stringUnion(["0", "1"])); expectType>>(true); @@ -183,7 +183,7 @@ describe("array", () => { describe("record", () => { test("basic", () => { - type Registers = ReturnType; + type Registers = Infer; const registersDecoder = record(stringUnion(["0", "1"])); expectType>>(true); @@ -223,7 +223,7 @@ describe("record", () => { ); expectType< - TypeEqual, Array> + TypeEqual, Array> >(true); const good = { "\\d{4}:\\d{2}": "Year/month", ".*": "Rest" }; @@ -267,7 +267,7 @@ describe("fieldsAuto", () => { fieldsAuto([string]); test("basic", () => { - type Person = ReturnType; + type Person = Infer; const personDecoder = fieldsAuto({ id: number, firstName: string, @@ -308,7 +308,7 @@ describe("fieldsAuto", () => { }); test("optional and renamed fields", () => { - type Person = ReturnType; + type Person = Infer; const personDecoder = fieldsAuto({ id: number, firstName: field(string, { renameFrom: "first_name" }), @@ -571,7 +571,7 @@ describe("fieldsAuto", () => { describe("fieldsUnion", () => { test("basic", () => { - type Shape = ReturnType; + type Shape = Infer; const shapeDecoder = fieldsUnion("tag", [ { tag: tag("Circle"), @@ -683,9 +683,9 @@ describe("fieldsUnion", () => { { tag: tag("A", { renameFieldFrom: "type" }) }, { tag: tag("B", { renameFieldFrom: "type" }) }, ]); - expectType< - TypeEqual, { tag: "A" } | { tag: "B" }> - >(true); + expectType, { tag: "A" } | { tag: "B" }>>( + true, + ); expect(decoder({ type: "A" })).toStrictEqual({ tag: "A" }); expect(decoder({ type: "B" })).toStrictEqual({ tag: "B" }); }); @@ -786,7 +786,7 @@ describe("fieldsUnion", () => { describe("tag", () => { test("basic", () => { const { decoder } = tag("Test"); - expectType, "Test">>(true); + expectType, "Test">>(true); expect(decoder("Test")).toBe("Test"); expect(run(decoder, "other")).toMatchInlineSnapshot(` At root: @@ -797,7 +797,7 @@ describe("tag", () => { test("renamed", () => { const { decoder } = tag("Test", { renameTagFrom: "test" }); - expectType, "Test">>(true); + expectType, "Test">>(true); expect(decoder("test")).toBe("Test"); expect(run(decoder, "other")).toMatchInlineSnapshot(` At root: @@ -814,7 +814,7 @@ describe("tuple", () => { tuple(number); test("0 items", () => { - type Type = ReturnType; + type Type = Infer; const decoder = tuple([]); expectType>(true); @@ -830,7 +830,7 @@ describe("tuple", () => { }); test("1 item", () => { - type Type = ReturnType; + type Type = Infer; const decoder = tuple([number]); expectType>(true); @@ -852,7 +852,7 @@ describe("tuple", () => { }); test("2 items", () => { - type Type = ReturnType; + type Type = Infer; const decoder = tuple([number, string]); expectType>(true); @@ -880,7 +880,7 @@ describe("tuple", () => { }); test("3 items", () => { - type Type = ReturnType; + type Type = Infer; const decoder = tuple([number, string, boolean]); expectType>(true); @@ -902,7 +902,7 @@ describe("tuple", () => { }); test("4 items", () => { - type Type = ReturnType; + type Type = Infer; const decoder = tuple([number, string, boolean, number]); expectType>(true); @@ -944,7 +944,7 @@ describe("tuple", () => { describe("multi", () => { test("basic", () => { - type Id = ReturnType; + type Id = Infer; const idDecoder = multi(["string", "number"]); expectType< @@ -973,7 +973,7 @@ describe("multi", () => { }); test("basic – mapped", () => { - type Id = ReturnType; + type Id = Infer; const idDecoder = chain(multi(["string", "number"]), (value) => { switch (value.type) { case "string": @@ -1006,7 +1006,7 @@ describe("multi", () => { }); test("basic – variation", () => { - type Id = ReturnType; + type Id = Infer; const idDecoder = chain(multi(["string", "number"]), (value) => { switch (value.type) { case "string": @@ -1135,7 +1135,7 @@ describe("undefinedOr", () => { test("undefined or string", () => { const decoder = undefinedOr(string); - expectType, string | undefined>>(true); + expectType, string | undefined>>(true); expect(decoder(undefined)).toBeUndefined(); expect(decoder("a")).toBe("a"); @@ -1150,7 +1150,7 @@ describe("undefinedOr", () => { test("with default", () => { const decoder = undefinedOr(string, "def"); - expectType, string>>(true); + expectType, string>>(true); expect(decoder(undefined)).toBe("def"); expect(decoder("a")).toBe("a"); @@ -1159,14 +1159,14 @@ describe("undefinedOr", () => { test("with other type default", () => { const decoder = undefinedOr(string, 0); - expectType, number | string>>(true); + expectType, number | string>>(true); expect(decoder(undefined)).toBe(0); expect(decoder("a")).toBe("a"); }); test("using with fieldsAuto does NOT result in an optional field", () => { - type Person = ReturnType; + type Person = Infer; const personDecoder = fieldsAuto({ name: string, age: undefinedOr(number), @@ -1252,7 +1252,7 @@ describe("nullable", () => { test("nullable string", () => { const decoder = nullable(string); - expectType, string | null>>(true); + expectType, string | null>>(true); expect(decoder(null)).toBeNull(); expect(decoder("a")).toBe("a"); @@ -1267,7 +1267,7 @@ describe("nullable", () => { test("with default", () => { const decoder = nullable(string, "def"); - expectType, string>>(true); + expectType, string>>(true); expect(decoder(null)).toBe("def"); expect(decoder("a")).toBe("a"); @@ -1276,7 +1276,7 @@ describe("nullable", () => { test("with other type default", () => { const decoder = nullable(string, 0); - expectType, number | string>>(true); + expectType, number | string>>(true); expect(decoder(null)).toBe(0); expect(decoder("a")).toBe("a"); @@ -1285,14 +1285,14 @@ describe("nullable", () => { test("with undefined instead of null", () => { const decoder = nullable(string, undefined); - expectType, string | undefined>>(true); + expectType, string | undefined>>(true); expect(decoder(null)).toBeUndefined(); expect(decoder("a")).toBe("a"); }); test("nullable field", () => { - type Person = ReturnType; + type Person = Infer; const personDecoder = fieldsAuto({ name: string, age: nullable(number), @@ -1338,7 +1338,7 @@ describe("nullable", () => { }); test("nullable autoField", () => { - type Person = ReturnType; + type Person = Infer; const personDecoder = fieldsAuto({ name: string, age: nullable(number),