fork of hey-api/openapi-ts because I need some additional things

Merge pull request #3120 from hey-api/copilot/fix-wrong-discriminator-issue

authored by Lubos and committed by GitHub f662e84c 85b90712

Changed files
+135 -58
.changeset
packages
openapi-ts
src
ir
openApi
3.0.x
parser
3.1.x
parser
plugins
@hey-api
typescript
openapi-ts-tests
main
test
__snapshots__
3.0.x
discriminator-all-of
discriminator-allof-nested
transformers-all-of
3.1.x
discriminator-all-of
discriminator-allof-nested
transformers-all-of
+5
.changeset/happy-points-run.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **plugin(@hey-api/typescript)**: improve type narrowing in discriminated types
+6 -6
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-all-of/types.gen.ts
··· 8 8 id: string; 9 9 }; 10 10 11 - export type Bar = Foo & { 11 + export type Bar = Omit<Foo, 'id'> & { 12 12 bar?: string; 13 13 id: 'Bar'; 14 14 }; 15 15 16 - export type Baz = Foo & { 16 + export type Baz = Omit<Foo, 'id'> & { 17 17 baz?: string; 18 18 id: 'Baz'; 19 19 }; 20 20 21 - export type Qux = Foo & { 21 + export type Qux = Omit<Foo, 'id'> & { 22 22 qux?: boolean; 23 23 id: 'Qux'; 24 24 }; ··· 27 27 id: string; 28 28 }; 29 29 30 - export type BarMapped = FooMapped & { 30 + export type BarMapped = Omit<FooMapped, 'id'> & { 31 31 bar?: string; 32 32 id: 'bar'; 33 33 }; 34 34 35 - export type BazMapped = FooMapped & { 35 + export type BazMapped = Omit<FooMapped, 'id'> & { 36 36 baz?: string; 37 37 id: 'baz'; 38 38 }; 39 39 40 - export type QuxMapped = FooMapped & { 40 + export type QuxMapped = Omit<FooMapped, 'id'> & { 41 41 qux?: boolean; 42 42 id: 'QuxMapped'; 43 43 };
+3 -3
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-allof-nested/types.gen.ts
··· 9 9 id: number; 10 10 }; 11 11 12 - export type CarDto = VehicleDto & { 12 + export type CarDto = Omit<VehicleDto, '$type'> & { 13 13 modelName: string; 14 - $type: 'Car' | 'Volvo'; 14 + $type: 'Car'; 15 15 }; 16 16 17 - export type VolvoDto = CarDto & { 17 + export type VolvoDto = Omit<CarDto, '$type'> & { 18 18 seatbeltCount: number; 19 19 $type: 'Volvo'; 20 20 };
+1 -1
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-all-of/types.gen.ts
··· 13 13 bar: 'foo' | 'bar' | 'baz'; 14 14 }; 15 15 16 - export type Baz = Qux & { 16 + export type Baz = Omit<Qux, 'id'> & { 17 17 foo: number; 18 18 bar: Date; 19 19 baz: 'foo' | 'bar' | 'baz';
+6 -6
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-all-of/types.gen.ts
··· 8 8 id: string; 9 9 }; 10 10 11 - export type Bar = Foo & { 11 + export type Bar = Omit<Foo, 'id'> & { 12 12 bar?: string; 13 13 id: 'Bar'; 14 14 }; 15 15 16 - export type Baz = Foo & { 16 + export type Baz = Omit<Foo, 'id'> & { 17 17 baz?: string; 18 18 id: 'Baz'; 19 19 }; 20 20 21 - export type Qux = Foo & { 21 + export type Qux = Omit<Foo, 'id'> & { 22 22 qux?: boolean; 23 23 id: 'Qux'; 24 24 }; ··· 27 27 id: string; 28 28 }; 29 29 30 - export type BarMapped = FooMapped & { 30 + export type BarMapped = Omit<FooMapped, 'id'> & { 31 31 bar?: string; 32 32 id: 'bar'; 33 33 }; 34 34 35 - export type BazMapped = FooMapped & { 35 + export type BazMapped = Omit<FooMapped, 'id'> & { 36 36 baz?: string; 37 37 id: 'baz'; 38 38 }; 39 39 40 - export type QuxMapped = FooMapped & { 40 + export type QuxMapped = Omit<FooMapped, 'id'> & { 41 41 qux?: boolean; 42 42 id: 'QuxMapped'; 43 43 };
+3 -3
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-allof-nested/types.gen.ts
··· 9 9 id: number; 10 10 }; 11 11 12 - export type CarDto = VehicleDto & { 12 + export type CarDto = Omit<VehicleDto, '$type'> & { 13 13 modelName: string; 14 - $type: 'Car' | 'Volvo'; 14 + $type: 'Car'; 15 15 }; 16 16 17 - export type VolvoDto = CarDto & { 17 + export type VolvoDto = Omit<CarDto, '$type'> & { 18 18 seatbeltCount: number; 19 19 $type: 'Volvo'; 20 20 };
+1 -1
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-all-of/types.gen.ts
··· 13 13 bar: 'foo' | 'bar' | 'baz'; 14 14 }; 15 15 16 - export type Baz = Qux & { 16 + export type Baz = Omit<Qux, 'id'> & { 17 17 foo: number; 18 18 bar: Date; 19 19 baz: 'foo' | 'bar' | 'baz';
+5
packages/openapi-ts/src/ir/types.d.ts
··· 183 183 */ 184 184 logicalOperator?: 'and' | 'or'; 185 185 /** 186 + * When used with `$ref` or `symbolRef`, specifies properties to omit from the referenced schema. 187 + * Useful for handling discriminator property conflicts in allOf compositions. 188 + */ 189 + omit?: ReadonlyArray<string>; 190 + /** 186 191 * When type is `object`, `patternProperties` can be used to define a schema 187 192 * for properties that match a specific regex pattern. 188 193 */
+43 -18
packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts
··· 80 80 }; 81 81 82 82 /** 83 - * Gets all discriminator values for a schema and its children in the inheritance hierarchy. 84 - * For intermediate schemas (those that are extended by others), returns a union of all values. 83 + * Gets the discriminator value for a schema. 84 + * Returns only the schema's own discriminator value, not child values. 85 85 */ 86 86 const getAllDiscriminatorValues = ({ 87 - context, 88 87 discriminator, 89 88 schemaRef, 90 89 }: { 91 - context: Context; 92 90 discriminator: NonNullable<SchemaObject['discriminator']>; 93 91 schemaRef: string; 94 92 }): Array<string> => { ··· 101 99 if (mappedSchemaRef === schemaRef) { 102 100 // This is the current schema's own value 103 101 values.push(value); 104 - continue; 105 - } 106 - 107 - // Check if the mapped schema extends the current schema 108 - const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 109 - if (mappedSchema.allOf) { 110 - for (const item of mappedSchema.allOf) { 111 - if ('$ref' in item && item.$ref === schemaRef) { 112 - // This schema extends the current schema, add its value 113 - values.push(value); 114 - break; 115 - } 116 - } 117 102 } 118 103 } 119 104 ··· 521 506 for (const { discriminator, isRequired, values } of discriminatorsToAdd) { 522 507 // Get all discriminator values including children for union types 523 508 const allValues = getAllDiscriminatorValues({ 524 - context, 525 509 discriminator, 526 510 schemaRef: state.$ref!, 527 511 }); ··· 543 527 logicalOperator: 'or', 544 528 } 545 529 : valueSchemas[0]!; 530 + 531 + // Check if any $ref schemas in schemaItems have this discriminator property 532 + // If yes, mark them to omit it to avoid conflicts 533 + for (const item of schemaItems) { 534 + if (item.$ref || item.symbolRef) { 535 + // Check if the referenced schema has this property 536 + const hasProperty = (() => { 537 + if (!item.$ref) return false; 538 + try { 539 + const refSchema = context.resolveRef<SchemaObject>(item.$ref); 540 + // Check if the discriminator property exists in the ref schema 541 + return ( 542 + refSchema.properties?.[discriminator.propertyName] !== 543 + undefined || 544 + (refSchema.allOf && 545 + refSchema.allOf.some((allOfItem) => { 546 + const resolved = 547 + '$ref' in allOfItem 548 + ? context.resolveRef<SchemaObject>(allOfItem.$ref) 549 + : allOfItem; 550 + return ( 551 + resolved.properties?.[discriminator.propertyName] !== 552 + undefined 553 + ); 554 + })) 555 + ); 556 + } catch { 557 + return false; 558 + } 559 + })(); 560 + 561 + if (hasProperty) { 562 + // Mark this ref to omit the discriminator property 563 + if (!item.omit) { 564 + item.omit = [discriminator.propertyName]; 565 + } else if (!item.omit.includes(discriminator.propertyName)) { 566 + item.omit = [...item.omit, discriminator.propertyName]; 567 + } 568 + } 569 + } 570 + } 546 571 547 572 // Find the inline schema (non-$ref) to merge the discriminator property into 548 573 // The inline schema should be the last non-$ref item in schemaItems
+42 -18
packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
··· 84 84 }; 85 85 86 86 /** 87 - * Gets all discriminator values for a schema and its children in the inheritance hierarchy. 88 - * For intermediate schemas (those that are extended by others), returns a union of all values. 87 + * Gets the discriminator value for a schema. 88 + * Returns only the schema's own discriminator value, not child values. 89 89 */ 90 90 const getAllDiscriminatorValues = ({ 91 - context, 92 91 discriminator, 93 92 schemaRef, 94 93 }: { 95 - context: Context; 96 94 discriminator: NonNullable<SchemaObject['discriminator']>; 97 95 schemaRef: string; 98 96 }): Array<string> => { ··· 105 103 if (mappedSchemaRef === schemaRef) { 106 104 // This is the current schema's own value 107 105 values.push(value); 108 - continue; 109 - } 110 - 111 - // Check if the mapped schema extends the current schema 112 - const mappedSchema = context.resolveRef<SchemaObject>(mappedSchemaRef); 113 - if (mappedSchema.allOf) { 114 - for (const item of mappedSchema.allOf) { 115 - if (item.$ref && item.$ref === schemaRef) { 116 - // This schema extends the current schema, add its value 117 - values.push(value); 118 - break; 119 - } 120 - } 121 106 } 122 107 } 123 108 ··· 602 587 for (const { discriminator, isRequired, values } of discriminatorsToAdd) { 603 588 // Get all discriminator values including children for union types 604 589 const allValues = getAllDiscriminatorValues({ 605 - context, 606 590 discriminator, 607 591 schemaRef: state.$ref!, 608 592 }); ··· 624 608 logicalOperator: 'or', 625 609 } 626 610 : valueSchemas[0]!; 611 + 612 + // Check if any $ref schemas in schemaItems have this discriminator property 613 + // If yes, mark them to omit it to avoid conflicts 614 + for (const item of schemaItems) { 615 + if (item.$ref || item.symbolRef) { 616 + // Check if the referenced schema has this property 617 + const hasProperty = (() => { 618 + if (!item.$ref) return false; 619 + try { 620 + const refSchema = context.resolveRef<SchemaObject>(item.$ref); 621 + // Check if the discriminator property exists in the ref schema 622 + return ( 623 + refSchema.properties?.[discriminator.propertyName] !== 624 + undefined || 625 + (refSchema.allOf && 626 + refSchema.allOf.some((allOfItem) => { 627 + const resolved = allOfItem.$ref 628 + ? context.resolveRef<SchemaObject>(allOfItem.$ref) 629 + : allOfItem; 630 + return ( 631 + resolved.properties?.[discriminator.propertyName] !== 632 + undefined 633 + ); 634 + })) 635 + ); 636 + } catch { 637 + return false; 638 + } 639 + })(); 640 + 641 + if (hasProperty) { 642 + // Mark this ref to omit the discriminator property 643 + if (!item.omit) { 644 + item.omit = [discriminator.propertyName]; 645 + } else if (!item.omit.includes(discriminator.propertyName)) { 646 + item.omit = [...item.omit, discriminator.propertyName]; 647 + } 648 + } 649 + } 650 + } 627 651 628 652 // Find the inline schema (non-$ref) to merge the discriminator property into 629 653 // The inline schema should be the last non-$ref item in schemaItems
+20 -2
packages/openapi-ts/src/plugins/@hey-api/typescript/v1/plugin.ts
··· 24 24 schema: IR.SchemaObject; 25 25 }): MaybeTsDsl<TypeTsDsl> => { 26 26 if (schema.symbolRef) { 27 - return $.type(schema.symbolRef); 27 + const baseType = $.type(schema.symbolRef); 28 + if (schema.omit && schema.omit.length > 0) { 29 + // Render as Omit<Type, 'prop1' | 'prop2'> 30 + const omittedKeys = 31 + schema.omit.length === 1 32 + ? $.type.literal(schema.omit[0]!) 33 + : $.type.or(...schema.omit.map((key) => $.type.literal(key))); 34 + return $.type('Omit').generics(baseType, omittedKeys); 35 + } 36 + return baseType; 28 37 } 29 38 30 39 if (schema.$ref) { ··· 33 42 resource: 'definition', 34 43 resourceId: schema.$ref, 35 44 }); 36 - return $.type(symbol); 45 + const baseType = $.type(symbol); 46 + if (schema.omit && schema.omit.length > 0) { 47 + // Render as Omit<Type, 'prop1' | 'prop2'> 48 + const omittedKeys = 49 + schema.omit.length === 1 50 + ? $.type.literal(schema.omit[0]!) 51 + : $.type.or(...schema.omit.map((key) => $.type.literal(key))); 52 + return $.type('Omit').generics(baseType, omittedKeys); 53 + } 54 + return baseType; 37 55 } 38 56 39 57 if (schema.type) {