Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
SimonSimCity opened this issue Jul 18, 2023 · 8 comments
Closed
Labels
Question An issue which isn't directly actionable in code

Comments

@SimonSimCity
Copy link

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;
};
@MartinJohns
Copy link
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.

@SimonSimCity
Copy link
Author

SimonSimCity commented Jul 18, 2023

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

@fatcerberus
Copy link

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.

@RyanCavanaugh
Copy link
Member

This behavior follows from the definition of the type, as @fatcerberus points out.

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

@SimonSimCity
Copy link
Author

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

@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).

@SimonSimCity
Copy link
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 😇

@fatcerberus
Copy link

@SimonSimCity This is a known pain point: see #54212 and #49886

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Jul 20, 2023
@typescript-bot
Copy link
Collaborator

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

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jul 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants