Skip to content

Commit

Permalink
Actions: expand isInputError to accept unknown (#11439)
Browse files Browse the repository at this point in the history
* feat: allow type `unknown` on `isInputError`

* chore: move ErrorInferenceObject to internal utils

* chore: changeset

* deps: expect-type

* feat: first types test

* chore: add types test to general test command

* refactor: use describe and it for organization
  • Loading branch information
bholmesdev authored Jul 9, 2024
1 parent ea8582f commit 08baf56
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 12 deletions.
18 changes: 18 additions & 0 deletions .changeset/nasty-poems-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'astro': patch
---

Expands the `isInputError()` utility from `astro:actions` to accept errors of any type. This should now allow type narrowing from a try / catch block.

```ts
// example.ts
import { actions, isInputError } from 'astro:actions';

try {
await actions.like(new FormData());
} catch (error) {
if (isInputError(error)) {
console.log(error.fields);
}
}
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"test:citgm": "pnpm -r --filter=astro test",
"test:match": "cd packages/astro && pnpm run test:match",
"test:unit": "cd packages/astro && pnpm run test:unit",
"test:types": "cd packages/astro && pnpm run test:types",
"test:unit:match": "cd packages/astro && pnpm run test:unit:match",
"test:smoke": "pnpm test:smoke:example && pnpm test:smoke:docs",
"test:smoke:example": "turbo run build --concurrency=100% --filter=\"@example/*\"",
Expand Down
4 changes: 3 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,13 @@
"build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.{ts,js}\" && pnpm run postbuild",
"dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.{ts,js}\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"",
"test": "pnpm run test:node",
"test": "pnpm run test:node && pnpm run test:types",
"test:match": "pnpm run test:node --match",
"test:e2e": "pnpm test:e2e:chrome && pnpm test:e2e:firefox",
"test:e2e:match": "playwright test -g",
"test:e2e:chrome": "playwright test",
"test:e2e:firefox": "playwright test --config playwright.firefox.config.js",
"test:types": "tsc --project tsconfig.tests.json",
"test:node": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
Expand Down Expand Up @@ -215,6 +216,7 @@
"astro-scripts": "workspace:*",
"cheerio": "1.0.0-rc.12",
"eol": "^0.9.1",
"expect-type": "^0.19.0",
"mdast-util-mdx": "^3.0.0",
"mdast-util-mdx-jsx": "^3.1.2",
"memfs": "^4.9.3",
Expand Down
11 changes: 11 additions & 0 deletions packages/astro/src/actions/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,14 @@ export async function getAction(
}
return actionLookup;
}

/**
* Used to preserve the input schema type in the error object.
* This allows for type inference on the `fields` property
* when type narrowed to an `ActionInputError`.
*
* Example: Action has an input schema of `{ name: z.string() }`.
* When calling the action and checking `isInputError(result.error)`,
* `result.error.fields` will be typed with the `name` field.
*/
export type ErrorInferenceObject = Record<string, any>;
10 changes: 2 additions & 8 deletions packages/astro/src/actions/runtime/virtual/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { z } from 'zod';
import { type ActionAPIContext, getApiContext as _getApiContext } from '../store.js';
import { type MaybePromise } from '../utils.js';
import {
ActionError,
ActionInputError,
type ErrorInferenceObject,
type SafeResult,
callSafely,
} from './shared.js';
import type { ErrorInferenceObject, MaybePromise } from '../utils.js';
import { ActionError, ActionInputError, type SafeResult, callSafely } from './shared.js';

export * from './shared.js';

Expand Down
8 changes: 5 additions & 3 deletions packages/astro/src/actions/runtime/virtual/shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { z } from 'zod';
import type { MaybePromise } from '../utils.js';
import type { ErrorInferenceObject, MaybePromise } from '../utils.js';

type ActionErrorCode =
| 'BAD_REQUEST'
Expand Down Expand Up @@ -40,8 +40,6 @@ const statusToCodeMap: Record<number, ActionErrorCode> = Object.entries(codeToSt
{}
);

export type ErrorInferenceObject = Record<string, any>;

export class ActionError<T extends ErrorInferenceObject = ErrorInferenceObject> extends Error {
type = 'AstroActionError';
code: ActionErrorCode = 'INTERNAL_SERVER_ERROR';
Expand Down Expand Up @@ -85,6 +83,10 @@ export class ActionError<T extends ErrorInferenceObject = ErrorInferenceObject>

export function isInputError<T extends ErrorInferenceObject>(
error?: ActionError<T>
): error is ActionInputError<T>;
export function isInputError(error?: unknown): error is ActionInputError<ErrorInferenceObject>;
export function isInputError<T extends ErrorInferenceObject>(
error?: unknown | ActionError<T>
): error is ActionInputError<T> {
return error instanceof ActionInputError;
}
Expand Down
31 changes: 31 additions & 0 deletions packages/astro/test/types/is-input-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expectTypeOf } from 'expect-type';
import { isInputError, defineAction } from '../../dist/actions/runtime/virtual/server.js';
import { z } from '../../zod.mjs';
import { describe, it } from 'node:test';

const exampleAction = defineAction({
input: z.object({
name: z.string(),
}),
handler: () => {},
});

const result = await exampleAction.safe({ name: 'Alice' });

describe('isInputError', () => {
it('isInputError narrows unknown error types', async () => {
try {
await exampleAction({ name: 'Alice' });
} catch (e) {
if (isInputError(e)) {
expectTypeOf(e.fields).toEqualTypeOf<Record<string, string[] | undefined>>();
}
}
});

it('`isInputError` preserves `fields` object type for ActionError objects', async () => {
if (isInputError(result.error)) {
expectTypeOf(result.error.fields).toEqualTypeOf<{ name?: string[] }>();
}
});
});
9 changes: 9 additions & 0 deletions packages/astro/tsconfig.tests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"include": ["test/types"],
"compilerOptions": {
"allowJs": true,
"emitDeclarationOnly": false,
"noEmit": true,
}
}
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 08baf56

Please sign in to comment.