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

T extends unknown is observably-different from an unconstrained T #61242

Open
mkantor opened this issue Feb 21, 2025 · 2 comments
Open

T extends unknown is observably-different from an unconstrained T #61242

mkantor opened this issue Feb 21, 2025 · 2 comments

Comments

@mkantor
Copy link
Contributor

mkantor commented Feb 21, 2025

🔎 Search Terms

"extends unknown" "type parameter" "generic" "conditional type" "keyof" "string" "assignable"

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about type parameter constraints

⏯ Playground Link

https://www.typescriptlang.org/play/?jsx=0#code/C4TwDgpgBAogHgQwLZgDYQDwBUB8UC8UWUEcwEAdgCYDOUA3gL5QD8UA1hCAPYBmRUAFxQawAE4BLCgHMAULIDG3CqKgBXCuwrcA7hQDCy0WIRTgBKNhJlKtdZu16cACjjD4yNJlwBKYcalpAjw4eSUVc21DCJMzC2wXN1hEFHQEvxFxQOCoOCA

💻 Code

type Example<T> = T extends {} ? keyof T : string

const unknownConstraint = <T extends unknown>(x: Example<T>): string => x

const noConstraint = <T>(x: Example<T>): string => x
//                                                 ^
// Type 'string | number | symbol' is not assignable to type 'string'.
//   Type 'number' is not assignable to type 'string'.

🙁 Actual behavior

The T extends unknown version behaves differently from the unconstrained version.

🙂 Expected behavior

The two versions should have the same behavior, probably both being rejected since code like this is unsafe:

const key = Symbol()
const bad = unknownConstraint<{ [key]: unknown }>(key)
bad.toUpperCase() // boom

Additional information about the issue

I was asked about this and eventually narrowed down the behavior to the example above.

#61203 is also possibly related?

@LukeAbby
Copy link

My theory is that this is for backwards compatibility when symbol keys were being added.

You can also get this behavior with any type whenever the apparent type of keyof T does NOT extend symbol | number.

For example:

const objectConstraint = <T extends { abc: number }>(x: Example<T>): string => x // Ok
const numberConstraint = <T extends 123>(x: Example<T>): string => x // Ok
//                                                            ^ `keyof 123` happens to be only strings so also okay.

Notably this means that T extends {} actually errors. This furthers my hypothesis that the test is extends symbol | number ? error : do nothing because never technically extends symbol | number. To further this T extends {} | 1 doesn't error.

This does still leave unknown as a second special case though.

@mkantor
Copy link
Contributor Author

mkantor commented Feb 21, 2025

#56652 seems possibly-relevant as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants