Skip to content

Commit

Permalink
Make Cakes invariant on T (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinyaodu authored Jan 27, 2023
1 parent 04cfd49 commit f19f134
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 13 deletions.
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,29 @@ Person.asShape(carol);
> 1. Runtime type-checking is often used to validate untrusted parsed JSON values from network requests. Strict type-checking is typically more appropriate for this use case.
> 2. It's easier to find and fix bugs caused by excessively strict type-checking, because values that should be okay will produce visible type errors instead. If the type-checking is too lenient, values that should produce type errors will be considered okay, which could have unexpected effects in other parts of your codebase.
### Linking TypeScript Types and Cakes

If you have a TypeScript type and a corresponding Cake, you can link them by adding a type annotation to the Cake:

```ts
const Person: Cake<Person> = bake(...);
```

This ensures that the Cake always represents the specified type exactly. If you change the type without updating the Cake, or vice versa, you'll get a TypeScript type error:

```ts
type Person = {
name: string;
lovesCake: boolean;
};

const Person: Cake<Person> = bake({ name: string });
// Property 'lovesCake' is missing in type '{ name: string; }' but required in type 'Person'.
```

### Inferring TypeScript Types from Cakes

If you don't want the Person type and its Cake to duplicate each other, you can delete the existing definition of the Person type, and infer the Person type from its Cake:
If you want, you can also delete the existing definition of the Person type, and infer the Person type from its Cake:

```ts
import { Infer } from "caketype";
Expand All @@ -167,12 +187,6 @@ type Person = Infer<typeof Person>;
// { name: string, age?: number | undefined }
```

> If you want to keep the explicit type definition, or the type is defined externally, use a type annotation to ensure that your Cake represents the desired type:
>
> ```ts
> const Person: Cake<Person> = bake(...);
> ```
### Creating Complex Cakes

More complex types, with nested objects and arrays, are also supported:
Expand Down
2 changes: 2 additions & 0 deletions etc/caketype.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export abstract class Cake<in out T = any> extends Untagged implements CakeArgs
abstract dispatchCheck(value: unknown, context: CakeDispatchCheckContext): CakeError | null;
// (undocumented)
abstract dispatchStringify(context: CakeDispatchStringifyContext): string;
// @internal
readonly _ENSURE_INVARIANT?: (value: T) => T;
is(value: unknown): value is T;
isShape(value: unknown): value is T;
// (undocumented)
Expand Down
9 changes: 9 additions & 0 deletions src/cake/Cake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,15 @@ abstract class Cake<in out T = any> extends Untagged implements CakeArgs {
abstract dispatchStringify(context: CakeDispatchStringifyContext): string;

abstract withName(name: string | null): Cake<T>;

/**
* Used for type inference to ensure that Cakes are invariant on T.
*
* This property is never used at runtime.
*
* @internal
*/
readonly _ENSURE_INVARIANT?: (value: T) => T;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/cake/Cake-withName.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const cakes = {
array: array({}),
literal: bake(0),
object: bake({}),
reference: reference(() => boolean),
reference: reference<boolean>(() => boolean),
tuple: new TupleCake({
startElements: [],
optionalElements: [],
Expand Down
26 changes: 21 additions & 5 deletions tests/readme/getting-started.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
string,
} from "../../src";
import type { FlagExactOptionalPropertyTypes } from "../../src/typescript-flags";
import { expectTypeError } from "../test-helpers";
import { expectTypeError, typeCheckOnly } from "../test-helpers";

test("Getting Started", () => {
type Person = {
Expand Down Expand Up @@ -105,15 +105,31 @@ test("Getting Started", () => {
});
}

typeCheckOnly(() => {
type Person = {
name: string;
lovesCake: boolean;
};

// @ts-expect-error
const Person: Cake<Person> = bake({ name: string });

// @ts-expect-error
const _wrongType: Cake<Person> = bake({ name: string, lovesCake: number });

// @ts-expect-error
const _extraProperty: Cake<Person> = bake({
name: string,
lovesCake: boolean,
extra: string,
});
});

{
type InferredPerson = Infer<typeof Person>;
type _ = Assert<Equivalent<Person, InferredPerson>>;
}

{
const _: Cake<Person> = Person;
}

{
type Account = {
person: Person;
Expand Down

0 comments on commit f19f134

Please sign in to comment.