From 4e95425defb49c56f2655e17d44799271a09af1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vesa=20V=C3=A4nsk=C3=A4?= Date: Thu, 2 Apr 2026 17:42:03 +0300 Subject: [PATCH] fix(openapi-typescript): strip null from FlattenedDeepRequired property types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FlattenedDeepRequired made all properties required but didn't strip `| null` from property types. When the enum-values type annotation path traversed a nullable intermediate object (e.g. `submission: {…} | null`), TypeScript couldn't access nested properties because `keyof (ObjType | null)` resolves to `never`. Use `NonNullable` instead of bare `T[K]` so nullable objects are unwrapped before the recursive mapped type enumerates their keys. --- packages/openapi-typescript/src/lib/ts.ts | 2 +- .../openapi-typescript/test/node-api.test.ts | 70 +++++++++++++++++-- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/packages/openapi-typescript/src/lib/ts.ts b/packages/openapi-typescript/src/lib/ts.ts index d1f41eb88..16cb1893d 100644 --- a/packages/openapi-typescript/src/lib/ts.ts +++ b/packages/openapi-typescript/src/lib/ts.ts @@ -353,7 +353,7 @@ export function tsArrayLiteralExpression( ) ) { const helper = stringToAST( - "type FlattenedDeepRequired = { [K in keyof T]-?: FlattenedDeepRequired[number] : T[K]>; };", + "type FlattenedDeepRequired = { [K in keyof T]-?: FlattenedDeepRequired extends unknown[] ? Extract, unknown[]>[number] : NonNullable>; };", )[0] as any; options.injectFooter.push(helper); } diff --git a/packages/openapi-typescript/test/node-api.test.ts b/packages/openapi-typescript/test/node-api.test.ts index b22f8ad18..4c5c95cdc 100644 --- a/packages/openapi-typescript/test/node-api.test.ts +++ b/packages/openapi-typescript/test/node-api.test.ts @@ -991,7 +991,7 @@ export interface components { } export type $defs = Record; type FlattenedDeepRequired = { - [K in keyof T]-?: FlattenedDeepRequired[number] : T[K]>; + [K in keyof T]-?: FlattenedDeepRequired extends unknown[] ? Extract, unknown[]>[number] : NonNullable>; }; type ReadonlyArray = [ Exclude @@ -1046,7 +1046,7 @@ export interface components { } export type $defs = Record; type FlattenedDeepRequired = { - [K in keyof T]-?: FlattenedDeepRequired[number] : T[K]>; + [K in keyof T]-?: FlattenedDeepRequired extends unknown[] ? Extract, unknown[]>[number] : NonNullable>; }; type ReadonlyArray = [ Exclude @@ -1225,7 +1225,7 @@ export interface operations { }; } type FlattenedDeepRequired = { - [K in keyof T]-?: FlattenedDeepRequired[number] : T[K]>; + [K in keyof T]-?: FlattenedDeepRequired extends unknown[] ? Extract, unknown[]>[number] : NonNullable>; }; type ReadonlyArray = [ Exclude @@ -1457,7 +1457,7 @@ export interface components { } export type $defs = Record; type FlattenedDeepRequired = { - [K in keyof T]-?: FlattenedDeepRequired[number] : T[K]>; + [K in keyof T]-?: FlattenedDeepRequired extends unknown[] ? Extract, unknown[]>[number] : NonNullable>; }; type ReadonlyArray = [ Exclude @@ -1552,7 +1552,7 @@ export interface components { } export type $defs = Record; type FlattenedDeepRequired = { - [K in keyof T]-?: FlattenedDeepRequired[number] : T[K]>; + [K in keyof T]-?: FlattenedDeepRequired extends unknown[] ? Extract, unknown[]>[number] : NonNullable>; }; type ReadonlyArray = [ Exclude @@ -1653,7 +1653,7 @@ export interface components { } export type $defs = Record; type FlattenedDeepRequired = { - [K in keyof T]-?: FlattenedDeepRequired[number] : T[K]>; + [K in keyof T]-?: FlattenedDeepRequired extends unknown[] ? Extract, unknown[]>[number] : NonNullable>; }; type ReadonlyArray = [ Exclude @@ -1769,7 +1769,7 @@ export interface components { } export type $defs = Record; type FlattenedDeepRequired = { - [K in keyof T]-?: FlattenedDeepRequired[number] : T[K]>; + [K in keyof T]-?: FlattenedDeepRequired extends unknown[] ? Extract, unknown[]>[number] : NonNullable>; }; type ReadonlyArray = [ Exclude @@ -1799,6 +1799,62 @@ export const resourceOuterOneOf0InnerOneOf1DeepCodeValues: ReadonlyArray["schemas"]["Resource"]["outer"], { kind: unknown; }>["kind"]> = ["typeB"]; +export type operations = Record;`, + options: { enumValues: true }, + }, + ], + [ + "options > enumValues with nullable intermediate object", + { + given: { + openapi: "3.1", + info: { title: "Test", version: "1.0" }, + components: { + schemas: { + Submission: { + type: "object", + properties: { + details: { + type: ["object", "null"], + properties: { + status: { + type: "string", + enum: ["draft", "submitted"], + }, + }, + }, + }, + }, + }, + }, + }, + want: `export type paths = Record; +export type webhooks = Record; +export interface components { + schemas: { + Submission: { + details?: { + /** @enum {string} */ + status?: "draft" | "submitted"; + } | null; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +type FlattenedDeepRequired = { + [K in keyof T]-?: FlattenedDeepRequired extends unknown[] ? Extract, unknown[]>[number] : NonNullable>; +}; +type ReadonlyArray = [ + Exclude +] extends [ + unknown[] +] ? Readonly> : Readonly[]>; +export const submissionDetailsStatusValues: ReadonlyArray["schemas"]["Submission"]["details"]["status"]> = ["draft", "submitted"]; export type operations = Record;`, options: { enumValues: true }, },