@@ -134,8 +134,8 @@ function isParameterObject(obj: OapiRefResolved | undefined): obj is ParameterOb
134134 return Boolean ( obj && ! isOasRef ( obj ) && obj . in ) ;
135135}
136136
137- function addIndexedAccess ( node : ts . TypeReferenceNode | ts . IndexedAccessTypeNode , ...segments : readonly string [ ] ) {
138- return segments . reduce ( ( acc , segment ) => {
137+ function addIndexedAccess ( node : ts . TypeNode , ...segments : readonly string [ ] ) {
138+ return segments . reduce < ts . TypeNode > ( ( acc , segment ) => {
139139 return ts . factory . createIndexedAccessTypeNode (
140140 acc ,
141141 ts . factory . createLiteralTypeNode (
@@ -147,6 +147,31 @@ function addIndexedAccess(node: ts.TypeReferenceNode | ts.IndexedAccessTypeNode,
147147 } , node ) ;
148148}
149149
150+ /**
151+ * Wrap a type with Extract<T, { propertyName: unknown }> to narrow a union type
152+ * before accessing a property that only exists on some variants.
153+ */
154+ function wrapWithExtract ( type : ts . TypeNode , propertyName : string ) : ts . TypeNode {
155+ return ts . factory . createTypeReferenceNode ( ts . factory . createIdentifier ( "Extract" ) , [
156+ type ,
157+ ts . factory . createTypeLiteralNode ( [
158+ ts . factory . createPropertySignature (
159+ /* modifiers */ undefined ,
160+ /* name */ ts . factory . createIdentifier ( propertyName ) ,
161+ /* questionToken */ undefined ,
162+ /* type */ ts . factory . createKeywordTypeNode ( ts . SyntaxKind . UnknownKeyword ) ,
163+ ) ,
164+ ] ) ,
165+ ] ) ;
166+ }
167+
168+ export interface OapiRefOptions {
169+ /** Whether to wrap with FlattenedDeepRequired<> (default: false) */
170+ deep ?: boolean ;
171+ /** Array of property names to wrap with Extract<> when accessing */
172+ extractProperties ?: string [ ] ;
173+ }
174+
150175/**
151176 * Convert OpenAPI ref into TS indexed access node (ex: `components["schemas"]["Foo"]`)
152177 * `path` is a JSON Pointer to a location within an OpenAPI document.
@@ -163,14 +188,18 @@ function addIndexedAccess(node: ts.TypeReferenceNode | ts.IndexedAccessTypeNode,
163188 * them according to their type; path, query, header, etc… so in these cases we
164189 * must check the parameter definition to determine the how to index into
165190 * the openapi-typescript type.
191+ * * Union variant properties (oneOf/anyOf)
192+ * When accessing properties that may only exist on some variants of a union type,
193+ * we use Extract<> to narrow the type before each property access.
166194 **/
167- export function oapiRef ( path : string , resolved ?: OapiRefResolved , deep = false ) : ts . TypeNode {
195+ export function oapiRef ( path : string , resolved ?: OapiRefResolved , options : OapiRefOptions = { } ) : ts . TypeNode {
168196 const { pointer } = parseRef ( path ) ;
169197 if ( pointer . length === 0 ) {
170198 throw new Error ( `Error parsing $ref: ${ path } . Is this a valid $ref?` ) ;
171199 }
172200
173201 const parametersObject = isParameterObject ( resolved ) ;
202+ const extractSet = new Set ( options . extractProperties ?? [ ] ) ;
174203
175204 // Initial segments are handled in a fixed , then remaining segments are treated
176205 // according to heuristics based on the initial segments
@@ -180,12 +209,14 @@ export function oapiRef(path: string, resolved?: OapiRefResolved, deep = false):
180209
181210 const leadingType = addIndexedAccess (
182211 ts . factory . createTypeReferenceNode (
183- ts . factory . createIdentifier ( deep ? `FlattenedDeepRequired<${ String ( initialSegment ) } >` : String ( initialSegment ) ) ,
212+ ts . factory . createIdentifier (
213+ options . deep ? `FlattenedDeepRequired<${ String ( initialSegment ) } >` : String ( initialSegment ) ,
214+ ) ,
184215 ) ,
185216 ...leadingSegments ,
186217 ) ;
187218
188- return restSegments . reduce < ts . TypeReferenceNode | ts . IndexedAccessTypeNode > ( ( acc , segment , index , original ) => {
219+ return restSegments . reduce < ts . TypeNode > ( ( acc , segment , index , original ) => {
189220 // Skip `properties` items when in the middle of the pointer
190221 // See: https://github.com/openapi-ts/openapi-typescript/issues/1742
191222 if ( segment === "properties" ) {
@@ -196,6 +227,14 @@ export function oapiRef(path: string, resolved?: OapiRefResolved, deep = false):
196227 return addIndexedAccess ( acc , resolved . in , resolved . name ) ;
197228 }
198229
230+ // If this segment is in the extractProperties list,
231+ // wrap the current type with Extract<T, { segment: unknown }> before accessing.
232+ // This narrows union types to variants that have this property.
233+ if ( extractSet . has ( segment ) ) {
234+ const narrowedType = wrapWithExtract ( acc , segment ) ;
235+ return addIndexedAccess ( narrowedType , segment ) ;
236+ }
237+
199238 return addIndexedAccess ( acc , segment ) ;
200239 } , leadingType ) ;
201240}
0 commit comments