Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/commons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@
"dist"
],
"scripts": {
"build": "run build:clean && run tsc",
"build": "run build:clean && run tsc --project tsconfig.build.json",
"build:clean": "run rimraf dist",
"format": "run prettier --write '**/*.{ts,tsx,yaml,yml,json,md,mdx,js}'",
"lint": "run eslint .",
"test": "node --experimental-vm-modules $(yarn bin jest)"
"test": "",
"test:compile": "run tsc --noEmit",
"test:unit": "node --experimental-vm-modules $(yarn bin jest)"
},
"dependencies": {
"@types/parse-path": "^7.0.3",
Expand Down Expand Up @@ -65,7 +67,6 @@
"react": "^18.3.1",
"rimraf": "^5.0.10",
"ts-jest": "^29.2.5",
"tsd": "^0.31.2",
"typescript": "^5.7.2"
},
"peerDependencies": {
Expand Down
124 changes: 66 additions & 58 deletions packages/commons/src/types/RequestType.test-types.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,110 @@
import { expectAssignable } from "tsd";
import { RequestType } from "./index.js";

/*
* Type-level tests. They are checked by `tsc --noEmit` (the `test:compile`
* target), not by a test runner. Assignability is asserted with `satisfies`
* rather than a type-level `extends`, so that excess-property and weak-type
* checks apply -- those are what a real call site passing an object literal
* gets, and they are what most of the assertions below are about. Expected
* rejections are marked with `@ts-expect-error`.
*/

type Data = { foo: string };
type Path = { bar: string };
type Header = { baz: string };
type Query = { whut: string };

function ignoredTestEmptyRequestTypes() {
expectAssignable<RequestType>({});
void ({} satisfies RequestType);
// @ts-expect-error Not assignable
expectAssignable<RequestType>({ extra: true });
void ({ extra: true } satisfies RequestType);
// @ts-expect-error Not assignable
expectAssignable<RequestType>({ data: {} });
void ({ data: {} } satisfies RequestType);
// @ts-expect-error Not assignable
expectAssignable<RequestType>({ data: null });
void ({ data: null } satisfies RequestType);
// @ts-expect-error Not assignable
expectAssignable<RequestType>({ pathParameters: {} });
void ({ pathParameters: {} } satisfies RequestType);
}

function ignoredTestRequestTypesWithDataType() {
expectAssignable<RequestType<Data>>({ data: { foo: "" } });
void ({ data: { foo: "" } } satisfies RequestType<Data>);
// @ts-expect-error Not assignable
expectAssignable<RequestType<Data>>({});
expectAssignable<RequestType<Data>>({
void ({} satisfies RequestType<Data>);
void ({
// @ts-expect-error Not assignable
data: { foo: "", extra: "" },
});
} satisfies RequestType<Data>);
// @ts-expect-error Not assignable
expectAssignable<RequestType<Data>>({ data: { noFoo: "" } });
void ({ data: { noFoo: "" } } satisfies RequestType<Data>);
}

function ignoredTestRequestTypesWithPathParameters() {
expectAssignable<RequestType<Data, Path>>({
void ({
data: { foo: "" },
pathParameters: { bar: "" },
});
expectAssignable<RequestType<null, Path>>({
} satisfies RequestType<Data, Path>);
void ({
pathParameters: { bar: "" },
});
} satisfies RequestType<null, Path>);
// @ts-expect-error Not assignable
expectAssignable<RequestType<null, Path>>({});
expectAssignable<RequestType<null, Path>>({
void ({} satisfies RequestType<null, Path>);
void ({
// @ts-expect-error Not assignable
pathParameters: {},
});
expectAssignable<RequestType<null, Path>>({
} satisfies RequestType<null, Path>);
void ({
// @ts-expect-error Not assignable
pathParameters: { foo: "", extra: "" },
});
} satisfies RequestType<null, Path>);
}

function ignoredTestRequestTypesWithHeader() {
expectAssignable<RequestType<Data, Path, null, Header>>({
void ({
data: {
foo: "",
},
pathParameters: { bar: "" },
headers: { baz: "" },
});
expectAssignable<RequestType<null, Path, null, Header>>({
} satisfies RequestType<Data, Path, null, Header>);
void ({
pathParameters: { bar: "" },
headers: { baz: "" },
});
expectAssignable<RequestType<null, null, null, Header>>({
} satisfies RequestType<null, Path, null, Header>);
void ({
headers: { baz: "" },
});
} satisfies RequestType<null, null, null, Header>);

// @ts-expect-error Not assignable
expectAssignable<RequestType<null, null, null, Header>>({});
expectAssignable<RequestType<null, null, null, Header>>({
void ({} satisfies RequestType<null, null, null, Header>);
void ({
headers: {
// @ts-expect-error Not assignable
baz: 42,
},
});
} satisfies RequestType<null, null, null, Header>);

expectAssignable<RequestType<null, null, null, Header>>({
void ({
// @ts-expect-error Not assignable
headers: {},
});
expectAssignable<RequestType<null, null, null, Header>>({
} satisfies RequestType<null, null, null, Header>);
void ({
// @ts-expect-error Not assignable
data: {},
headers: {
baz: "",
},
});
expectAssignable<RequestType<null, null, null, Header>>({
} satisfies RequestType<null, null, null, Header>);
void ({
// @ts-expect-error Not assignable
pathParameters: {},
headers: {
baz: "",
},
});
} satisfies RequestType<null, null, null, Header>);
}

function ignoredTestRequestTypesWithQuery() {
expectAssignable<RequestType<Data, Path, Query, Header>>({
void ({
data: {
foo: "",
},
Expand All @@ -105,58 +113,58 @@ function ignoredTestRequestTypesWithQuery() {
queryParameters: {
whut: "",
},
});
expectAssignable<RequestType<null, Path, Query, Header>>({
} satisfies RequestType<Data, Path, Query, Header>);
void ({
pathParameters: { bar: "" },
headers: { baz: "" },
queryParameters: {
whut: "",
},
});
expectAssignable<RequestType<null, null, Query, null>>({
} satisfies RequestType<null, Path, Query, Header>);
void ({
queryParameters: {
whut: "",
},
});
} satisfies RequestType<null, null, Query, null>);

// @ts-expect-error Not assignable
expectAssignable<RequestType<null, null, Query, null>>({});
expectAssignable<RequestType<null, null, Query, null>>({
void ({} satisfies RequestType<null, null, Query, null>);
void ({
queryParameters: {
// @ts-expect-error Not assignable
whut: 42,
},
});
} satisfies RequestType<null, null, Query, null>);

expectAssignable<RequestType<null, null, Query, null>>({
void ({
// @ts-expect-error Not assignable
queryParameters: {},
});
} satisfies RequestType<null, null, Query, null>);
}

function ignoredTestAdditionalHeadersCanAlwaysBeSet() {
expectAssignable<RequestType<null>>({
void ({
headers: { extra: true },
});
expectAssignable<RequestType<null, null>>({
} satisfies RequestType<null>);
void ({
headers: { extra: true },
});
expectAssignable<RequestType<null, null, null>>({
} satisfies RequestType<null, null>);
void ({
headers: { extra: true },
});
expectAssignable<RequestType<Data>>({
} satisfies RequestType<null, null, null>);
void ({
data: {
foo: "",
},
headers: { extra: true },
});
expectAssignable<RequestType<null, Path>>({
} satisfies RequestType<Data>);
void ({
pathParameters: {
bar: "",
},
headers: { extra: true },
});
expectAssignable<RequestType<null, null, null, Header>>({
} satisfies RequestType<null, Path>);
void ({
headers: { extra: true, baz: "" },
});
} satisfies RequestType<null, null, null, Header>);
}
83 changes: 62 additions & 21 deletions packages/commons/src/types/Response.test-types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
import { expectAssignable, expectNotAssignable, expectType } from "tsd";
import { InternalAxiosRequestConfig } from "axios";
import { Response } from "./Response.js";

/*
* Type-level tests, checked by `tsc --noEmit` (the `test:compile` target).
*
* `satisfies` asserts assignability the way a real call site sees it, i.e.
* including the excess-property checks that only apply to object literals
* passed into a typed position. `ExpectExact<Equals<...>>` asserts exact type
* identity, which `satisfies` cannot express -- it would also accept a type
* that is merely assignable. Expected rejections are marked
* with `@ts-expect-error`.
*/

/**
* Resolves to `true` only if both types are identical, not merely mutually
* assignable. The two deferred signatures are only related to each other when
* `TActual` and `TExpected` resolve identically, which is the one way plain
* TypeScript can express exactness.
*/
type Equals<TActual, TExpected> =
(<T>() => T extends TActual ? 1 : 2) extends <T>() => T extends TExpected
? 1
: 2
? true
: false;

/** Fails to compile unless the `Equals<...>` handed to it resolved to `true`. */
type ExpectExact<TIgnoredMatch extends true> = void;

type Response200 = Response<{ a: string }, 200, "application/json">;
type Response200Text = Response<{ text: string }, 200, "text/plain">;

Expand All @@ -12,54 +38,63 @@ const additionalAxiosResponseData = {
mediaType: "application/json",
} as const;

expectAssignable<Response200>({
void ({
data: { a: "" },
status: 200,
...additionalAxiosResponseData,
});
expectNotAssignable<Response200>({
data: { a: "", extra: "!" },
} satisfies Response200);
void ({
data: {
a: "",
// @ts-expect-error Not assignable, `extra` is not part of the response data
extra: "!",
},
status: 200,
...additionalAxiosResponseData,
});
} satisfies Response200);

type Response201 = Response<{ b: string }, 201>;
expectAssignable<Response201>({
void ({
data: {
b: "",
},
status: 201,
...additionalAxiosResponseData,
});
} satisfies Response201);

type Response400 = Response<null, 400>;
expectAssignable<Response400>({
void ({
data: null,
status: 400,
...additionalAxiosResponseData,
});
expectNotAssignable<Response400>({
} satisfies Response400);
void ({
data: null,
// @ts-expect-error Not assignable, 42 is not the response status
status: 42,
...additionalAxiosResponseData,
});
expectNotAssignable<Response400>({
} satisfies Response400);
void ({
data: null,
status: 42,
status: 400,
// @ts-expect-error Not assignable, `extra` is not part of the response
extra: "!",
...additionalAxiosResponseData,
});
expectNotAssignable<Response400>({
} satisfies Response400);
void ({
// @ts-expect-error Not assignable, the response carries no data
data: { extraContent: "" },
status: 400,
...additionalAxiosResponseData,
});
} satisfies Response400);

function ignoredTestRequestTypesWithDataPathParameters() {
type SomeResponse = Response200 | Response200Text | Response201 | Response400;
const someResponse = {} as SomeResponse;

expectType<200 | 201 | 400>(someResponse.status);
type ignoredStatusIsExactlyTheDeclaredOnes = ExpectExact<
Equals<typeof someResponse.status, 200 | 201 | 400>
>;

if (someResponse.status === 200) {
// @ts-expect-error > a is not in data
Expand All @@ -70,14 +105,20 @@ function ignoredTestRequestTypesWithDataPathParameters() {
if (someResponse.mediaType === "text/plain") {
// @ts-expect-error > a is not in data
someResponse.data.a;
expectType<string>(someResponse.data.text);
type ignoredTextIsString = ExpectExact<
Equals<typeof someResponse.data.text, string>
>;
} else {
// @ts-expect-error > text is not in data
someResponse.data.text;
expectType<string>(someResponse.data.a);
type ignoredAIsString = ExpectExact<
Equals<typeof someResponse.data.a, string>
>;
}
} else if (someResponse.status === 201) {
expectType<string>(someResponse.data.b);
type ignoredBIsString = ExpectExact<
Equals<typeof someResponse.data.b, string>
>;
// @ts-expect-error > a is not in data
someResponse.data.a;
}
Expand Down
Loading
Loading