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
50 changes: 50 additions & 0 deletions .changeset/string-templates-drop-typescript-dep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
"openapi-typescript": major
---

feat: generate TypeScript with raw string templates and drop the TypeScript dependency

`openapi-typescript` no longer uses the TypeScript compiler API (`ts.factory`,
`createPrinter`, `createSourceFile`) at runtime. Generation now builds the
`.d.ts` source with string templates, so the package

- works with **TypeScript 7** (the native compiler), which ships no classic
compiler API — this fixes the `Cannot read properties of undefined (reading
'createKeywordTypeNode')` crash,
- has **no runtime TypeScript dependency**: `typescript` is no longer a peer
dependency and is not resolved at all when generating,
- produces the same output as 7.x: every committed example snapshot, the test
suite, and a differential run over 27 option combinations are byte-for-byte
identical.

Three deliberate output differences, all of them fixes for invalid or redundant
output (see the PR description for reproductions):

- `tsUnion()` / `tsIntersection()` no longer emit a redundant single-member
union, so `(string)[][]` is now `string[][]`. Semantically identical.
- A multi-line `x-enum-descriptions` entry no longer leaks a bare token into the
enum body (it used to produce invalid TypeScript); line breaks become spaces.
- With `pathParamsAsTypes`, a URL containing a backtick or `${` no longer breaks
out of the generated template literal type.

**Breaking changes**

- `openapiTS()` now resolves to a `string` (the generated file body, without the
comment header) instead of an array of AST nodes. The body ends with a newline.
- The transform hooks exchange plain strings instead of AST nodes:
- `transform` returns `string | { schema: string; questionToken: boolean }`
- `postTransform` receives and returns `string`
- `transformProperty` receives and returns `{ name, optional, type, comment?, indent }`;
use the new `tsComment()` helper to attach JSDoc from that hook
- `GlobalContext.injectFooter` is now a `FooterDeclaration[]` (generated strings,
plus a deferred `OperationsDeclaration` for the `operations` interface)
- Callbacks that used to build AST nodes with a separately installed `typescript`
must return the equivalent type text instead, e.g.
`ts.factory.createTypeReferenceNode("Date")` becomes `"Date"`. `typescript` is
no longer required to use this package at all.
- The AST-oriented helpers `stringToAST()`, `tsModifiers()` and `QUESTION_TOKEN`
were removed and are replaced by string builders: `typeLiteral()`,
`tupleType()`, `propertySignature()`, `indexSignature()`, `typeAlias()`,
`interfaceDecl()`, `enumDecl()`, `INDENT` and `tsComment()`.
- The `inject` option is now emitted verbatim instead of being re-printed by the
TypeScript printer.
2 changes: 1 addition & 1 deletion docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ _Note: OpenAPI 2.x is supported with versions `5.x` and previous_
This library requires the latest version of [Node.js](https://nodejs.org) installed (20.x or higher recommended). With that present, run the following in your project:

```bash
npm i -D openapi-typescript typescript
npm i -D openapi-typescript
```

And in your `tsconfig.json`, to load the types properly:
Expand Down
85 changes: 29 additions & 56 deletions docs/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The Node API may be useful if dealing with dynamically-created schemas, or you
## Setup

```bash
npm i --save-dev openapi-typescript typescript
npm i --save-dev openapi-typescript
```

::: tip Recommended
Expand All @@ -31,18 +31,21 @@ The Node.js API accepts either a `URL`, `string`, or JSON object as input:

It also accepts `Readable` streams and `Buffer` types that are resolved and treated as strings (validation, bundling, and type generation can’t really happen without the whole document).

The Node API returns a `Promise` with a TypeScript AST. You can then traverse / manipulate / modify the AST as you see fit.
The Node API returns a `Promise` that resolves to the generated TypeScript source, ready to write to a file.

To convert the TypeScript AST into a string, you can use `astToString()` helper which is a thin wrapper around [TypeScript’s printer](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#re-printing-sections-of-a-typescript-file):
::: tip

Because generation no longer uses the TypeScript compiler API, `openapi-typescript` works with any TypeScript version — including TypeScript 7 — and doesn’t require `typescript` to be installed at all. The `astToString()` helper is still exported for code written against older versions; it now simply normalizes the source into a file body ending in a newline.

:::

::: code-group

```ts [src/my-project.ts]
import fs from "node:fs";
import openapiTS, { astToString } from "openapi-typescript";
import openapiTS from "openapi-typescript";

const ast = await openapiTS(new URL("./my-schema.yaml", import.meta.url));
const contents = astToString(ast);
const contents = await openapiTS(new URL("./my-schema.yaml", import.meta.url));

// (optional) write to file
fs.writeFileSync("./my-schema.ts", contents);
Expand Down Expand Up @@ -74,7 +77,7 @@ const redocly = await createConfig(
// option 2: load from redocly.yaml file
const redocly = await loadConfig({ configPath: "redocly.yaml" });

const ast = await openapiTS(mySchema, { redocly });
const types = await openapiTS(mySchema, { redocly });
```

:::
Expand All @@ -97,7 +100,7 @@ The Node API supports all the [CLI flags](/cli#flags) in `camelCase` format, plu
Use the `transform()` and `postTransform()` options to override the default Schema Object transformer with your own. This is useful for providing nonstandard modifications for specific parts of your schema.

- `transform()` runs **before** the conversion to TypeScript (you’re working with the original OpenAPI nodes)
- `postTransform()` runs **after** the conversion to TypeScript (you’re working with TypeScript AST)
- `postTransform()` runs **after** the conversion to TypeScript (you’re working with the generated type text)

#### Example: `Date` types

Expand All @@ -116,17 +119,14 @@ By default, openapiTS will generate `updated_at?: string;` because it’s not su

```ts [src/my-project.ts]
import openapiTS from "openapi-typescript";
import ts from "typescript";

const DATE = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Date")); // `Date`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`
const DATE = "Date"; // `Date`
const NULL = "null"; // `null`

const ast = await openapiTS(mySchema, {
const types = await openapiTS(mySchema, {
transform(schemaObject, metadata) {
if (schemaObject.format === "date-time") {
return schemaObject.nullable
? ts.factory.createUnionTypeNode([DATE, NULL])
: DATE;
return schemaObject.nullable ? `${DATE} | ${NULL}` : DATE;
}
},
});
Expand Down Expand Up @@ -168,17 +168,14 @@ Use the same pattern to transform the types:

```ts [src/my-project.ts]
import openapiTS from "openapi-typescript";
import ts from "typescript";

const BLOB = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Blob")); // `Blob`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`
const BLOB = "Blob"; // `Blob`
const NULL = "null"; // `null`

const ast = await openapiTS(mySchema, {
const types = await openapiTS(mySchema, {
transform(schemaObject, metadata) {
if (schemaObject.format === "binary") {
return schemaObject.nullable
? ts.factory.createUnionTypeNode([BLOB, NULL])
: BLOB;
return schemaObject.nullable ? `${BLOB} | ${NULL}` : BLOB;
}
},
});
Expand Down Expand Up @@ -221,18 +218,15 @@ Here we return an object with a schema property, which is the same as the above

```ts [src/my-project.ts]
import openapiTS from "openapi-typescript";
import ts from "typescript";

const BLOB = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Blob")); // `Blob`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`
const BLOB = "Blob"; // `Blob`
const NULL = "null"; // `null`

const ast = await openapiTS(mySchema, {
const types = await openapiTS(mySchema, {
transform(schemaObject, metadata) {
if (schemaObject.format === "binary") {
return {
schema: schemaObject.nullable
? ts.factory.createUnionTypeNode([BLOB, NULL])
: BLOB,
schema: schemaObject.nullable ? `${BLOB} | ${NULL}` : BLOB,
questionToken: true,
};
}
Expand Down Expand Up @@ -263,7 +257,7 @@ Use the `transformProperty()` option to modify individual property signatures wi

- `transformProperty()` runs **after** type conversion but **before** JSDoc comments are added
- It receives the property signature, the original schema object, and transformation options
- It should return a modified `PropertySignature` or `undefined` to leave the property unchanged
- It should return a modified `{ name, optional, type, comment?, indent }` object, or `undefined` to leave the property unchanged

#### Example: JSDoc validation annotations

Expand Down Expand Up @@ -297,10 +291,9 @@ components:

```ts [src/my-project.ts]
import fs from "node:fs";
import ts from "typescript";
import openapiTS, { astToString } from "openapi-typescript";
import openapiTS, { tsComment } from "openapi-typescript";

const ast = await openapiTS(mySchema, {
const contents = await openapiTS(mySchema, {
transformProperty(property, schemaObject, options) {
const validationTags: string[] = [];

Expand All @@ -326,33 +319,13 @@ const ast = await openapiTS(mySchema, {

// If we have validation tags, add them as JSDoc comments
if (validationTags.length > 0) {
// Create a new property signature
const newProperty = ts.factory.updatePropertySignature(
property,
property.modifiers,
property.name,
property.questionToken,
property.type,
);

// Add JSDoc comment
const jsDocText = `*\n * ${validationTags.join('\n * ')}\n `;

ts.addSyntheticLeadingComment(
newProperty,
ts.SyntaxKind.MultiLineCommentTrivia,
jsDocText,
true,
);

return newProperty;
return { ...property, comment: tsComment(validationTags, property.indent) };
}

return property;
},
});

const contents = astToString(ast);
fs.writeFileSync("./my-schema.ts", contents);
```

Expand Down Expand Up @@ -388,6 +361,6 @@ export interface components {
:::

The `transformProperty` function provides access to:
- `property`: The TypeScript PropertySignature AST node
- `property`: The generated property signature as a plain object — `{ name, optional, type, comment?, indent }`. Return the same shape to replace it, or `undefined` to keep it unchanged. Use `tsComment()` to attach JSDoc.
- `schemaObject`: The original OpenAPI Schema Object for this property
- `options`: Transformation context including path information and other utilities
3 changes: 0 additions & 3 deletions packages/openapi-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@
"prepublish": "pnpm run build",
"version": "pnpm run build"
},
"peerDependencies": {
"typescript": "^5.x"
},
"dependencies": {
"@redocly/openapi-core": "^1.34.6",
"ansi-colors": "^4.1.3",
Expand Down
8 changes: 4 additions & 4 deletions packages/openapi-typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { performance } from "node:perf_hooks";
import type { Readable } from "node:stream";
import { createConfig } from "@redocly/openapi-core";
import type ts from "typescript";
import { validateAndBundle } from "./lib/redoc.js";
import type { TSNode } from "./lib/ts.js";
import { debug, resolveRef, scanDiscriminators } from "./lib/utils.js";
import transformSchema from "./transform/index.js";
import type { GlobalContext, OpenAPI3, OpenAPITSOptions } from "./types.js";
Expand Down Expand Up @@ -34,7 +34,7 @@ export const COMMENT_HEADER = `/**
`;

/**
* Convert an OpenAPI schema to TypesScript AST
* Convert an OpenAPI schema to TypeScript type declarations
* @param {string|URL|object|Readable} source OpenAPI schema source:
* - YAML: string
* - JSON: parsed object
Expand All @@ -44,7 +44,7 @@ export const COMMENT_HEADER = `/**
export default async function openapiTS(
source: string | URL | OpenAPI3 | Buffer | Readable,
options: OpenAPITSOptions = {} as Partial<OpenAPITSOptions>,
): Promise<ts.Node[]> {
): Promise<TSNode> {
if (!source) {
throw new Error("Empty schema. Please specify a URL, file path, or Redocly Config");
}
Expand Down Expand Up @@ -101,7 +101,7 @@ export default async function openapiTS(
};

const transformT = performance.now();
const result = transformSchema(schema, ctx);
const result = `${transformSchema(schema, ctx).join("\n")}\n`;
debug("Completed AST transformation for entire document", "ts", performance.now() - transformT);

return result;
Expand Down
Loading
Loading