Skip to content

Optional nested readonly type changes type of property adding undefined #55058

Closed as not planned

Description

Bug Report

I was working with a library which reported that a property had an additional value of undefined, and the author of the plugin claimed that the error went by removing readonly. After investigating I found the following, which I found confusing.

The framework we use (vue) has a function for marking objects as deeply readonly:

export declare function readonly<T extends object>(target: T): DeepReadonly<T>;
export declare type DeepReadonly<T> = T extends {} ? {
    readonly [K in keyof T]: DeepReadonly<T[K]>;
} : Readonly<T>;

When wrapping an object with optional properties:

export declare class User {
    a?: string;
    b: string;
}
export const user = readonly(new User() )

... the exported types have an additional undefined at the type definition, even though it wasn't allowed in the original type.

export declare class User {
    a?: string;
    b: string;
}
export declare const user: {
    readonly a?: string | undefined;
    readonly b: string;
};

🔎 Search Terms

extra undefined, nested readonly, deepreadonly

🕗 Version & Regression Information

  • This is the behavior in every version I tried.

⏯ Playground Link

Playground link with relevant code

💻 Code

This was the smallest I could reduce the code to and still see the compiler doing the unexpected:

export declare function readonly<T extends object>(target: T): DeepReadonly<T>;
export declare type DeepReadonly<T> = T extends {} ? {
    readonly [K in keyof T]: DeepReadonly<T[K]>;
} : Readonly<T>;

export declare class User {
    a?: string;
    b: string;
}

export declare type ReadOnlyUser = DeepReadonly<User>

export const user = readonly(new User() )

🙁 Actual behavior

The code above is transformed to the following types. The generated type for the variable user is incompatible with the exported class User, because it adds undefined as type to the type of the optional parameter of the class.

export declare const user: {
    readonly a?: string | undefined;
    readonly b: string;
};

🙂 Expected behavior

The types of User and user should be identical, the latter should just have all props as readonly, but there should be no other changes to the type.

export declare const user: {
    readonly a?: string;
    readonly b: string;
};

Activity

  1. MartinJohns commented on Jul 18, 2023

    @MartinJohns
    Contributor

    What is exactOptionalPropertyTypes set to? If it's set to false there's no difference between the types. The additional undefined makes no difference.

  2. SimonSimCity commented on Jul 18, 2023

    @SimonSimCity
    Author

    Sorry, forgot to mention 😅 I've set exactOptionalPropertyTypes to true, as recommended.

  3. fatcerberus commented on Jul 18, 2023

    @fatcerberus

    I assume the problem is that, for optional properties, regardless of eOPT, T[K] evaluates to P | undefined, because that's the type you will get if you read from that property. In other words:

    const val: typeof obj[typeof key] = obj[key];

    which wouldn't hold if the undefined wasn't included for optional properties.

  4. RyanCavanaugh commented on Jul 18, 2023

    @RyanCavanaugh
    Member

    This behavior follows from the definition of the type, as Bruce Pascoe (@fatcerberus) points out.

    Turning on EOPT, I see the desired behavior. Can you clarify?

  5. SimonSimCity commented on Jul 19, 2023

    @SimonSimCity
    Author

    Oh! The generated types are different based on this option 🫣

    Ryan Cavanaugh (@RyanCavanaugh) Ok, let me explain ... The example itself is a bit more complicated 😊 Some of the code is taken from a code-basis, where I have no control over - an external library. This external library generates the types and does not have exactOptionalPropertyTypes enabled.

    I, in my project, want to have exactOptionalPropertyTypes enabled, which is why I am getting this error.

    Here's the example. I don't know about vue, but the other plugin (Auth0Vue) has it disabled. This means, the example is best split into two:

    1. https://www.typescriptlang.org/play?exactOptionalPropertyTypes=false&ts=5.0.4#code/PTAEGEHsBMFNQJYFsAOkBOAXW1QDN1IlQA3AV1gChYAPNLRAO23TwEMBjeASUYCtYHTAkiMA0rACeAHgAqAPlC1sjaAGdQAZUlIARpAA2oAN4BfanQyZQcDgbbp4eMoyEjGoFIRII4c+QAUANZSAFygvAJuohIyCqAAPqBqmOgIjADmADSkbAYU4bIAlOEkkL4A3JQW9Na29o74LtEejmzQogZxSjQq6qCQulGYgZgOGbCYhSWgACKwsCgASrDtnXHyVbS1NoIN8JiSKPDziytrjF3+oAC8oLI9fRpmoAD8JpSgX6BtHZeSoAA2mImKAQpJIHh7gBdcKnZarP5XWTA6GbSimUDhc5IjZVSj1BzwDiiFKgFZ4bR6QzhFwIACOFGSOn0Bi2lgY6RY7C45NgeDkt1AbEYkkUxk+3xIeQK9yq3yBFKprNhoFSFCq5m2Vl2diJTVcwlEP35-gC0vysGm2NNCnx2oYhMaerUGgAqmpYOgPgq2K9wik0pl5d9dP7kql0hlNdUQBAYPBkLUcPhCMQAIJkTAACwADAA1Cg1HVcr08+CZnMFijgAwIWDMH2INSV7MN4QcNjYaA2gX6QyrRjor5kT3oXvSD1exKgFxwPDpHDo8zFhgujSt3MABXyGXSiFQBlgSHbG6zecLsFr9cbEq+XgQ0uwoAA+ggW+f2whO92J-2jyKih3I4eABOwBiekUIYPk+8AvqOXoTlO3pJHO-KLtAQEmqBZhQdUXzvq2X4-imwGIusAQ5u+AB0b4fjmxFdjgeEjmOQq-BRVFqLRCHoHhK4OnUez6iSjBkumbqyAAErmL7cAAcgAUgAouAsjcAA8vJL5iMpACa4SRIIRriFI0ibpe17tuiQA
    2. https://www.typescriptlang.org/play?exactOptionalPropertyTypes=true&ts=5.0.4#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgSSQCthsYEIkBpYATwB4AVAPjlFSQBMBnOAZVoBbAEYQANnADeAKAC+00JFhxOpMZih50AVyRkKSOGCgQAbglVNmACgDWdAFwFipcpRoMWcAD5xuMKGQAcwAaOFNMMW1gJ0YASidTCAsAbgVwaHhVbHVNOB09N0NNTE5KMU82EA4eOAhhEjIbGA0g4BhYhLgAEWBgMAAlYFLyz2Y0xUyVNQ08GFowPF7+oZGkCqs4AF44Riqa3klZOAB+KWk4S7gSsvXaOABtKkRDe1oIdF2AXSdlweHbhtGE8vuM5HAnKtAWM0tlcnhsJR-HAhugBCJxE5dAgAI7RPxCURiCYZZTIVAYHB4VFMbZwTBIWisGRXcKRaKxNKsh6o9FEn5wALRNLySbKOGzfK6fSUa7AdBWawRKIxXZdGksElKLIzPI5TDcXgAVW4aHOrMwJyc-kCSCCXKuwitfgCwRF6W1LwpWFwcAAgtoYAALAAMADVogBhMQIYAoc1XBDcAPBuPkbCYVCcSHy+iicTDJBg1naU1QHMKk1m3y6VToZDAThg0WknX6vXqQ3+wOhgAKUSCyEQgjAYmAgjTvBToYjwGjsfjLKuxgQEVQcAA+knp2mEBmsw7Liu13gN6W0IfEMme7v942nEvWZcbqM2SqnPmxwzL6yX3dHryhLiAKQrAJesiXue5YJk+cprBUb4cjBsFXH+CGWtarp2j4cC1vKDacD+KFofcTqYbaQQ4Xh9ZII2RGXMcNZcPhtGERcxEAq+PLynywFOKB4HumKbbwnAiJIMifpGowAASIYbvgAByABSACikaMPgADyikblQqkAJpOIQjRFB49DTuGUYxmmYLCVIEHSNIAD0zlwJGECqAQADkph4AA7oEMAcHAAC0gpBginl4Jgoh+VeYkQGAsacPkJiCHAABEQbBWA3AOK5-lFQAdPMizcNggRgDA6h2sV0BBM5o6YLQJygDgMBadVBiRL2JiLLAtCMAswDcFsWBiKaABkMBjQArMVIbFQALAAxIiqjOb2jB+qpADiqkydwABCACyABiikAIrKQZ53JlptjHVpfoABoAOoAIxXd0imffgYhXX6ADMfphp9QSRkGBl+r2ikADIDH6WkAEzA4w-mqadx0kH6fBGgZkYwAZMk7bYCCnX6IZQH6kbAH6MlBH6OKw2IkafdwRCYH6e2cEDABaRpiPgfoDGAfp+ijEB+opy3HVgSBXbQ-NXdg3RBMIwhgMtwCnRAtCqUQe0QOd+ADPg+DLdgADUQNGjL-N8H6520EEph+sp2gQEG+C0JGLOwzix04oIWlBPgRB+t0ghR3wtjCH6BlGstwgi2IWmqbYtjw8tIPKVd70AJyKZgKOFxAADsy3wzAqnAEQggAF5XRAQT8yAfBEFdOIAGw4pGV3aGIe0GW3QRaXtwiRgZQZ8O9TORlQ-ncJGTN8FA3QwEgr0ybQYbCHwn2KRASkABzKQgfBUL23SNwgtABEQ53A9b0uRqfEDw-ghfoGLgjS90c+YA+BBFej3Y6MAZL82AHwaWktBD4EEFQP0V0wDKXwDJIMFcgjHU4NgKgSB+YaF7HND6O0e6Tz4AgQQRpuhBmwHNCO-Mwyd0+sdRu-lugIAQOdKgQRzr8xTgMHuV1G5BnOv5EWkYtLnT2nwLSSAQzHQQCjMQQQDI9wGAZCukZlpzUUm0botgYCfT9MIQyWcjQgFsNbYGIB8DACulQQutAcTA1oMdXhxhFJBhNnwc6-s5qCBRmGMQKNOAIGUowMM2B-JiFUr2YGc1QqQxDKYWg3A9rBMUopSMSAQBSxnqdTAaAjR7SZiHMABkw69iCDiFGzj-IIFsCGUcc1OCF04L2CutgcRiBDEGMQilOBGiusdROvZjrZyNNgXsQZIynSBvgZStgdq038lHCWn1V6fRkjEqAIZT7W0jPzJAF0EBECZpI963RToyRRkpPgYYK6nVlqrRSRADLcxRqAkRtAoADCINAOaqzgYQFeqYPg3RPqgD4edEA-kb5gBkt0CAp9G7nSDEEbQRA+AwGBpTY6r1aB7VelQ86Yy1F8BksIPaPdG6kHhtwc6UBC7YHMTia2r0ghXRDNofycDrZBCCO9a26AUb+SDFpQuKNlKhRRkQWgVAEBBmBpbemz1-LKOALYI0+ADK9hsafP0pgcRaVehARg6dgZoJklpUKVAa4oNUoIIO-MgxXRGa9RupqUavWWqFah-k9r4FLCgua2hrZ+mOiGYQy10ANxAOdTglzgCG1Dt3UKx0BhhnOjiRGkYZIQEwVQZaCijTAEbj3S+RApnaBEDiWgEtVJQEEPDCu2AJbYCZkaJmEBtB+k5p2v0p9TDKXhnNCm4tIyYGWnwc2ZtAimxDAgLWwBPoVxgNoBAQNMrSGMGYCwwBrDsVZFJWS8klJqQ0tpXS+kDIhFPVcI49JeC6FsEgCA-lDAGm7MGEM-ZtCDiQE+uI0ggA

    Is this expected? Well, I would assume, that projects are independent by best effort. Now this project forces me to disable exactOptionalPropertyTypes for my project, if they do not enable it or change the code (as of now, they've merged changes for the latter by removing the readonly wrapping).

  6. SimonSimCity commented on Jul 19, 2023

    @SimonSimCity
    Author

    I can't really come up with a good solution here. A project would almost have to know with which parameters a set of types are built, right ..? 🙈

    When I take my playground example, and flip the switch for exactOptionalPropertyTypes from true to false and back again, the only thing that changes is the property user.a, even though User.a also implicitly could be undefined, which is not stated in the generated types. This could is a type-insecurity when now using it in a project having exactOptionalPropertyTypes enabled.

    Maybe I'm also just going way off track ... I'm just trying to find a good solution 😇

  7. fatcerberus commented on Jul 19, 2023

    @fatcerberus

    Simon Schick (@SimonSimCity) This is a known pain point: see #54212 and #49886

  8. typescript-bot commented on Jul 23, 2023

    @typescript-bot
    Contributor

    This issue has been marked as "Question" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  9. locked as resolved and limited conversation to collaborators on Oct 22, 2025
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

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions