Skip to content

[BUG]: It's merging types that are not the same #3116

Description

@thany

Types that are "similar" I guess, but not quite the same, are being merged, leading to weird optional properties in the resulting types, where they should not be.

Issue Type

output

Context (Environment, Version, Language)

Input Format: json
Output Language: typescript

CLI, npm, or app.quicktype.io: npm & app
Version: 26.0.0

Description

I'm using it to generate types for strong-typed translation keys. And this bug is breaking that.

Input Data

{
  "User": {
    "Profile": {
      "Title": "Mijn profiel",
      "Error": "Serverfout, probeer het nog een keer",
      "Success": "Profiel succesvol bijgewerkt",
      "Submit": "Wijzig gegevens",
      "Fields": {
        "Email": "E-mailadres",
        "FirstName": "Voornaam",
        "LastName": "Achternaam"
      }
    },
    "ChangePassword": {
      "Title": "Wachtwoord wijzigen",
      "Error": "Wachtwoord wijzigen is mislukt. Probeer het later nog eens.",
      "Success": "Wachtwoord is gewijzigd. De volgende dat u inlogt, kunt u uw nieuwe wachtwoord gebruiken.",
      "Submit": "Wijzig wachtwoord",
      "Fields": {
        "OldPassword": "Huidig wachtwoord",
        "NewPassword": "Nieuw wachtwoord",
        "ConfirmPassword": "Herhaal wachtwoord ter bevestiging"
      }
    },
    "Notifications": {
      "Title": "Notificatievoorkeuren",
      "Error": "Notificatievoorkeuren konden niet worden bewaard. Probeer het later nog eens.",
      "Success": "Notificatievoorkeuren zijn bewaard.",
      "Submit": "Voorkeuren bewaren"
    }
  }
}

Expected Behaviour / Output

Do not merge types that are mergeable, so it should become something like:

export type Welcome = {
    User: User;
}

export type User = {
    Profile:        Profile;
    ChangePassword: ChangePassword;
    Notifications:  Notifications;
}

export type Profile = {
    Title:   string;
    Error:   string;
    Success: string;
    Submit:  string;
    Fields: ProfileFields;
}

export type Notifications = {
    Title:   string;
    Error:   string;
    Success: string;
    Submit:  string;
}

export type Profile = {
    Title:   string;
    Error:   string;
    Success: string;
    Submit:  string;
    Fields: ProfileFields;
}

export type ProfileFields = {
    Email:           string;
    FirstName:       string;
    LastName:        string;
}

export type ChangePasswordFields = {
    OldPassword:     string;
    NewPassword:     string;
    ConfirmPassword: string;
}

Current Behaviour / Output

Instead, it's just willy-nilly merging types that are obviously different, but just happen to be partially overlapping:

export type Welcome = {
    User: User;
}

export type User = {
    Profile:        ChangePassword;
    ChangePassword: ChangePassword;
    Notifications:  ChangePassword;
}

export type ChangePassword = {
    Title:   string;
    Error:   string;
    Success: string;
    Submit:  string;
    Fields?: Fields;
}

export type Fields = {
    OldPassword?:     string;
    NewPassword?:     string;
    ConfirmPassword?: string;
    Email?:           string;
    FirstName?:       string;
    LastName?:        string;
}

Why is it doing that?

Steps to Reproduce

  1. Paste the JSON above into the app
  2. Set output language to Typescript

Options are not relevant. And also, I think it does a similar thing in other output languages, although I'm not an expert in all of them.

Possible Solution

If partial overlaps must be merged for some reason, do it correctly, with extends. Not by smashing them together and making everything optional. Or better yet, to keep is simple - don't merge. At least don't produce an output that no longer matches the input precisely.

Activity

  1. oliver-kopcik commented on Aug 30, 2026

    @oliver-kopcik
    Contributor

    I reproduced this on master, and the merging is the combineClasses inference step doing what it is designed to do — but there is an existing opt-out that produces exactly the output you asked for.

    With --no-combine-classes:

    $ quicktype --lang ts --src-lang json --no-combine-classes input.json
    
    export interface User {
        Profile:        Profile;
        ChangePassword: ChangePassword;
        Notifications:  Notifications;
    }
    
    export interface Profile {
        Title:   string;
        Error:   string;
        Success: string;
        Submit:  string;
        Fields:  ProfileFields;
    }
    
    export interface ProfileFields {
        Email:     string;
        FirstName: string;
        LastName:  string;
    }
    
    export interface ChangePassword {
        Title:   string;
        Error:   string;
        Success: string;
        Submit:  string;
        Fields:  ChangePasswordFields;
    }
    
    export interface ChangePasswordFields {
        OldPassword:     string;
        NewPassword:     string;
        ConfirmPassword: string;
    }
    
    export interface Notifications {
        Title:   string;
        Error:   string;
        Success: string;
        Submit:  string;
    }

    That matches your expected output, including the separate ProfileFields / ChangePasswordFields and no optional properties. In the web app the same control is the "Combine similar classes" option.

    For the strongly-typed translation-keys use case this is probably what you want as a permanent setting, since the whole point there is that each key path is its own distinct shape and should never be unified with a sibling.

    Leaving the underlying question to the maintainers — whether the default is right for JSON input, and whether the docs should point at the flag more prominently — since changing the default would be a breaking change for existing users.

  2. thany commented on Aug 31, 2026

    @thany
    Author

    Isn't there a compromise? It looks like --no-combine-classes never combines classes, just as the name of the option implies. But maybe it makes sense to only combine classes that are identical, not just similar.

    Maybe the combine classes feature should provide 3 options:

    • Never combine
    • Only combine identical classes
    • Also combine similar classes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions