Compare commits
2 Commits
8ea85c4aab
...
a8e5f63415
| Author | SHA1 | Date | |
|---|---|---|---|
| a8e5f63415 | |||
| 1cab48ca48 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@robonen/fetch",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"license": "Apache-2.0",
|
||||
"description": "A lightweight, type-safe fetch wrapper with interceptors, retry, and V8-optimized internals",
|
||||
"keywords": [
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { assertType, describe, expectTypeOf, it } from 'vitest';
|
||||
|
||||
import type { FetchOptions } from './types';
|
||||
import { createFetch } from './fetch';
|
||||
|
||||
type Body = FetchOptions['body'];
|
||||
|
||||
describe('FetchOptions body', () => {
|
||||
it('accepts a named interface without a cast', () => {
|
||||
// The regression under test: a named `interface` has no implicit index
|
||||
// signature, so a `Record<string, unknown>` body type would reject it and
|
||||
// force a cast at every call site. It must assign directly.
|
||||
interface CreateUser { name: string; age: number }
|
||||
expectTypeOf<CreateUser>().toExtend<Body>();
|
||||
assertType<Body>({ name: 'Alice', age: 30 } satisfies CreateUser);
|
||||
});
|
||||
|
||||
it('accepts plain objects, arrays, BodyInit strings and null', () => {
|
||||
assertType<Body>({ a: 1 });
|
||||
assertType<Body>([1, 2, 3]);
|
||||
assertType<Body>('raw string');
|
||||
assertType<Body>(new FormData());
|
||||
assertType<Body>(null);
|
||||
});
|
||||
|
||||
it('rejects bare primitives', () => {
|
||||
// @ts-expect-error a bare number is not a valid request body
|
||||
assertType<Body>(42);
|
||||
// @ts-expect-error a bare boolean is not a valid request body
|
||||
assertType<Body>(true);
|
||||
});
|
||||
|
||||
it('lets a typed interface flow through the method shortcuts', () => {
|
||||
interface CreateDeal { title: string; amount: number }
|
||||
const $fetch = createFetch();
|
||||
const deal: CreateDeal = { title: 'x', amount: 1 };
|
||||
// Must type-check with no cast on the body.
|
||||
expectTypeOf($fetch.post).toBeCallableWith('/deals', { body: deal });
|
||||
});
|
||||
});
|
||||
@@ -79,8 +79,14 @@ export interface FetchOptions<R extends ResponseType = 'json', T = unknown>
|
||||
FetchHooks<T, R> {
|
||||
/** Base URL prepended to all relative request URLs */
|
||||
baseURL?: string;
|
||||
/** Request body — plain objects are automatically JSON-serialized */
|
||||
body?: RequestInit['body'] | Record<string, unknown> | unknown[] | null;
|
||||
/**
|
||||
* Request body. `BodyInit` values (string, Blob, FormData, streams, …) are
|
||||
* sent as-is; any other object or array is JSON-serialized. Typed as `object`
|
||||
* rather than `Record<string, unknown>` so a named `interface` assigns
|
||||
* directly — interfaces carry no implicit index signature and would otherwise
|
||||
* force every caller to cast the body.
|
||||
*/
|
||||
body?: RequestInit['body'] | object | null;
|
||||
/** Suppress throwing on 4xx/5xx responses */
|
||||
ignoreResponseError?: boolean;
|
||||
/** URL query parameters serialized and appended to the request URL */
|
||||
|
||||
@@ -3,5 +3,11 @@ import { defineConfig } from 'vitest/config';
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
// Type tests (*.test-d.ts) are statically analyzed with `tsc --noEmit`
|
||||
// alongside the runtime suite.
|
||||
typecheck: {
|
||||
enabled: true,
|
||||
tsconfig: './tsconfig.src.json',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user