Skip to content

Commit

Permalink
Add the Infer utility type
Browse files Browse the repository at this point in the history
  • Loading branch information
lydell committed Oct 23, 2023
1 parent 72e486c commit e7ee677
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 50 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 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<typeof myDecoder>` to `Infer<typeof myDecoder>` now!

### Version 15.0.0 (2023-10-23)

This release changes the options parameter of `fieldsAuto` and `fieldsUnion` from:
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
DecoderError,
field,
fieldsAuto,
type Infer,
number,
string,
} from "tiny-decoders";
Expand Down Expand Up @@ -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<typeof userDecoder2>;
type User2 = Infer<typeof userDecoder2>;
```

`User2` above is equivalent to the `User` type already shown earlier.
Expand Down Expand Up @@ -963,14 +964,16 @@ const personDecoder = fieldsAuto({
age: number,
});

type Person = ReturnType<typeof personDecoder>;
type Person = Infer<typeof personDecoder>;
// equivalent to:
type Person = {
name: string;
age: number;
};
```

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
Expand Down
7 changes: 4 additions & 3 deletions examples/readme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DecoderError,
field,
fieldsAuto,
Infer,
number,
repr,
ReprOptions,
Expand Down Expand Up @@ -143,7 +144,7 @@ test("fieldsAuto", () => {

type Example = { name?: string };

expectType<TypeEqual<ReturnType<typeof exampleDecoder>, Example>>(true);
expectType<TypeEqual<Infer<typeof exampleDecoder>, Example>>(true);

const exampleDecoder2 = fieldsAuto({
name: field(undefinedOr(string), { optional: true }),
Expand All @@ -156,7 +157,7 @@ test("fieldsAuto", () => {

type Example2 = { name?: string | undefined };

expectType<TypeEqual<ReturnType<typeof exampleDecoder2>, Example2>>(true);
expectType<TypeEqual<Infer<typeof exampleDecoder2>, Example2>>(true);

expect(exampleDecoder2({ name: undefined })).toStrictEqual({
name: undefined,
Expand Down Expand Up @@ -185,7 +186,7 @@ test("field", () => {
d?: string | undefined;
};

expectType<TypeEqual<ReturnType<typeof exampleDecoder>, Example>>(true);
expectType<TypeEqual<Infer<typeof exampleDecoder>, Example>>(true);

expect(exampleDecoder({ a: "", c: undefined })).toStrictEqual({
a: "",
Expand Down
4 changes: 2 additions & 2 deletions examples/renaming-union-field.test.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,7 +17,7 @@ test("using different tags in JSON and in TypeScript", () => {
},
]);

type InferredType = ReturnType<typeof decoder>;
type InferredType = Infer<typeof decoder>;
type ExpectedType =
| { tag: "Circle"; radius: number }
| { tag: "Square"; size: number };
Expand Down
15 changes: 8 additions & 7 deletions examples/type-inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
chain,
field,
fieldsAuto,
Infer,
multi,
number,
string,
Expand All @@ -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<typeof personDecoder>;
type Person = Infer<typeof personDecoder>;
expectType<TypeEqual<Person, { name: string; age: number }>>(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({
Expand All @@ -45,7 +46,7 @@ test("making a type from a decoder", () => {
});

// Then, let TypeScript infer the `User` type!
type User = ReturnType<typeof userDecoder>;
type User = Infer<typeof userDecoder>;
// Try hovering over `User` in the line above – your editor should reveal the
// exact shape of the type.

Expand Down Expand Up @@ -116,7 +117,7 @@ test("making a type from an object and stringUnion", () => {
expectType<TypeEqual<Severity, "Critical" | "High" | "Low" | "Medium">>(true);

const severityDecoder = stringUnion(SEVERITIES);
expectType<TypeEqual<Severity, ReturnType<typeof severityDecoder>>>(true);
expectType<TypeEqual<Severity, Infer<typeof severityDecoder>>>(true);
expect(severityDecoder("High")).toBe("High");

function coloredSeverity(severity: Severity): string {
Expand Down
5 changes: 2 additions & 3 deletions examples/untagged-union.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
chain,
Decoder,
fieldsAuto,
Infer,
number,
string,
undefinedOr,
Expand Down Expand Up @@ -85,9 +86,7 @@ test("tagged union, but using boolean instead of string", () => {
}),
);

type User =
| ReturnType<typeof adminDecoder>
| ReturnType<typeof notAdminDecoder>;
type User = Infer<typeof adminDecoder> | Infer<typeof notAdminDecoder>;

const userDecoder: Decoder<User> = (value) => {
const { isAdmin } = fieldsAuto({ isAdmin: boolean })(value);
Expand Down
6 changes: 4 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

export type Decoder<T, U = unknown> = (value: U) => T;

export type Infer<T extends Decoder<any>> = ReturnType<T>;

// Make VSCode show `{ a: string; b?: number }` instead of `{ a: string } & { b?: number }`.
// https://stackoverflow.com/a/57683652/2010616
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
Expand Down Expand Up @@ -109,9 +111,9 @@ type FieldsMapping = Record<string, Decoder<any> | Field<any, FieldMeta>>;

type InferField<T extends Decoder<any> | Field<any, FieldMeta>> =
T extends Field<any, FieldMeta>
? ReturnType<T["decoder"]>
? Infer<T["decoder"]>
: T extends Decoder<any>
? ReturnType<T>
? Infer<T>
: never;

type InferFields<Mapping extends FieldsMapping> = Expand<
Expand Down
Loading

0 comments on commit e7ee677

Please sign in to comment.