Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/fresh-types-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@konsistent/convention": patch
"konsistent": patch
---

fix(konsistent): represent exact TypeScript type references as string alternatives to JSON Schema shapes
2 changes: 1 addition & 1 deletion docs/guides/fixing-violations.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ Search first: grep for `X`, case variants, stripped variants (`createX` ↔ `X`,

For `exportConstants`, the value must be a `const`. If a `let` or `function` exists under the right name, conversion to `const` is safe only if there are no reassignments — verify by grepping.

When a constant entry includes `schema`, the constant must also have a matching explicit type annotation. Add or adjust the annotation only after confirming the initializer and all assignments satisfy it. Schema checks support scalar, literal-union enum, homogeneous array, and inline object annotations; array items and object properties may require an exact type reference. They do not infer initializer types or resolve referenced types. For object schemas, every configured property must be declared. Names in `required` must be non-optional, while other configured names must include `?`.
When a constant entry includes `schema`, the constant must also have a matching explicit type annotation. Add or adjust the annotation only after confirming the initializer and all assignments satisfy it. Schema checks support scalar, literal-union enum, homogeneous array, and inline object annotations; array items and object properties may use a string to require an exact TypeScript type reference. They do not infer initializer types or resolve referenced types. For object schemas, every configured property must be declared. Names in `required` must be non-optional, while other configured names must include `?`.

#### `exportTypes`

Expand Down
8 changes: 4 additions & 4 deletions docs/reference/predicates.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ exported type.
"schema": {
"type": "object",
"properties": {
"auth": { "type": "Readonly<MyAuth>" },
"auth": "Readonly<MyAuth>",
"model": { "type": "string" },
"timeout": { "type": "number" }
},
Expand Down Expand Up @@ -325,12 +325,12 @@ The supported `schema` forms are:

- Scalar: `{ "type": "string" }`, using `string`, `number`, `boolean`, or `null`.
- Enum: `{ "type": "string", "enum": ["a", "b"] }`. The annotation must contain exactly the configured literal values, although their order does not matter.
- Array: `{ "type": "array", "items": { "type": "string" } }`. Items may use a scalar type or a TypeScript type reference such as `MyAuth`, `Namespace.MyAuth`, or `Readonly<MyAuth>`. `T[]`, `Array<T>`, `readonly T[]`, and `ReadonlyArray<T>` annotations qualify.
- Object: `{ "type": "object", "properties": { ... }, "required": [...], "additionalProperties": false }`. Every configured property must be declared. An empty property schema (`{}`) checks its presence and optionality without constraining its type; a typed schema checks a scalar type or an exact TypeScript type reference.
- Array: `{ "type": "array", "items": { "type": "string" } }`. Items may use a scalar schema or a TypeScript type reference string such as `"MyAuth"`, `"Namespace.MyAuth"`, or `"Readonly<MyAuth>"`. `T[]`, `Array<T>`, `readonly T[]`, and `ReadonlyArray<T>` annotations qualify.
- Object: `{ "type": "object", "properties": { ... }, "required": [...], "additionalProperties": false }`. Every configured property must be declared. An empty property schema (`{}`) checks its presence and optionality without constraining its type; a scalar schema checks its JSON type; a string checks an exact TypeScript type reference.

Object schemas describe TypeScript declaration shapes rather than ordinary JSON Schema instances. Every name in `properties` must exist in the annotation or definition. Names listed in `required` must be non-optional (`name: Type`); all other configured names must be optional (`name?: Type`). `required` defaults to an empty array. `additionalProperties` defaults to `true`, allowing unconfigured TypeScript properties; set it to `false` to reject them.

Inner type references are compared directly to the source annotation without resolving imports or aliases. The comparison includes formatting, so `Readonly<MyAuth>` does not match `Readonly< MyAuth >`. Configured references may contain ASCII letters, digits, spaces, `_`, `$`, `.`, `<`, `>`, and `,`; leading or trailing spaces are rejected.
Inner TypeScript type references are compared directly to the source annotation without resolving imports or aliases. They support template substitutions, including transformations such as `"${harnessId.toPascalCase()}Data"`. The comparison includes formatting, so `Readonly<MyAuth>` does not match `Readonly< MyAuth >`.

This is deliberately a strict subset of JSON Schema. Unsupported keywords and shapes are rejected during configuration validation, including `integer`, `$ref`, combinators, nested object or array schemas, tuple schemas, enum array items, schema-valued `additionalProperties`, and constraints such as `minItems`.

Expand Down
4 changes: 2 additions & 2 deletions e2e/fixtures/constant-schemas-broken/konsistent.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"name": "localAuths",
"schema": {
"type": "array",
"items": { "type": "Readonly<MyAuth>" }
"items": "Readonly<MyAuth>"
}
}
],
Expand Down Expand Up @@ -55,7 +55,7 @@
"schema": {
"type": "object",
"properties": {
"auth": { "type": "Readonly<MyAuth>" },
"auth": "Readonly<MyAuth>",
"modelId": { "type": "string" }
},
"required": ["auth", "modelId"]
Expand Down
4 changes: 2 additions & 2 deletions e2e/fixtures/constant-schemas/konsistent.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"name": "localAuths",
"schema": {
"type": "array",
"items": { "type": "Readonly<MyAuth>" }
"items": "Readonly<MyAuth>"
}
}
],
Expand Down Expand Up @@ -56,7 +56,7 @@
"schema": {
"type": "object",
"properties": {
"auth": { "type": "Readonly<MyAuth>" },
"auth": "Readonly<MyAuth>",
"modelId": { "type": "string" }
},
"required": ["auth", "modelId"]
Expand Down
13 changes: 11 additions & 2 deletions e2e/fixtures/type-schemas-broken/konsistent.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"conventions": [
{
"name": "type-schemas",
"paths": "src/types.ts",
"paths": "src/{harnessId}.ts",
"must": {
"declareTypes": [
{
Expand All @@ -12,7 +12,16 @@
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"auth": { "type": "Readonly<MyAuth>" }
"auth": "Readonly<MyAuth>"
}
}
},
{
"name": "InternalDataSettings",
"schema": {
"type": "object",
"properties": {
"data": "${harnessId.toPascalCase()}Data"
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions e2e/fixtures/type-schemas-broken/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
type OtherAuth = { token: string };
type OtherData = { value: string };

type InternalSettings = {
enabled?: boolean;
auth?: Readonly<OtherAuth>;
};

type InternalDataSettings = { data?: OtherData };

export type ModuleSettings = {
model?: string;
reasoning?: "low" | "medium" | "high";
Expand Down
13 changes: 11 additions & 2 deletions e2e/fixtures/type-schemas/konsistent.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"conventions": [
{
"name": "type-schemas",
"paths": "src/types.ts",
"paths": "src/{harnessId}.ts",
"must": {
"declareTypes": [
{
Expand All @@ -12,7 +12,16 @@
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"auth": { "type": "Readonly<MyAuth>" }
"auth": "Readonly<MyAuth>"
}
}
},
{
"name": "InternalDataSettings",
"schema": {
"type": "object",
"properties": {
"data": "${harnessId.toPascalCase()}Data"
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions e2e/fixtures/type-schemas/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
type MyAuth = { token: string };
type TypesData = { value: string };

type InternalSettings = {
enabled?: boolean;
auth?: Readonly<MyAuth>;
};

type InternalDataSettings = { data?: TypesData };

export type ModuleSettings = {
model?: string;
timeout?: number;
Expand Down
3 changes: 3 additions & 0 deletions e2e/new-predicates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ describe("type-schemas-broken fixture", () => {
expect(error.stdout).toContain(
'Type "InternalSettings" property "auth" must be of type "Readonly<MyAuth>"'
);
expect(error.stdout).toContain(
'Type "InternalDataSettings" property "data" must be of type "TypesData"'
);
expect(error.stdout).toContain("type-schemas");
}
});
Expand Down
Loading