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

Empty array literals without contextual type can lead to unsound index accesses #57419

Closed
Andarist opened this issue Feb 15, 2024 · 11 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@Andarist
Copy link
Contributor

πŸ”Ž Search Terms

empty array literal contextual type declared type index access implicitnevertype

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.4.0-dev.20240215#code/MYewdgzgLgBCBGArGBeGBvAUDGBDATvgFwwDaAugDSYC+A3JpqJLAogOoCWUAFgJJgAJgFMAHiQBKw0PkEAeaPk5gA5pRjwQIADbDcYAHyoM9Rs2gxh242wB0BfKQAM5Budj5hEAK7bYaNi5eARFRUitXTAB6KJwcAD0AfiA

πŸ’» Code

const obj = {
  arr: [],
};

const objWithIndex: Record<string, boolean> = {};

const el = obj.arr[0];
//    ^? const el: never
const result = objWithIndex[el];
//    ^? const result: boolean

πŸ™ Actual behavior

never element type allows for just anything to happen here since never is assignable to everything

πŸ™‚ Expected behavior

I wouldn't expect this empty array literal to allow for this. Maybe it would be better to use an empty tuple in that situation? This would prevent obtaining never by indexing this array and thus it would prevent such accidents.

Additional information about the issue

No response

@MartinJohns
Copy link
Contributor

That's what noUncheckedIndexedAccess is for. When you enable it you end up with undefined, not never.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Feb 15, 2024
@RyanCavanaugh
Copy link
Member

I wouldn't expect this empty array literal to allow for this. Maybe it would be better to use an empty tuple in that situation?

This doesn't really help much, since [][number] is still never.

@rotu
Copy link

rotu commented Feb 16, 2024

This has always bothered me too.
If I create an empty array, it's perfectly reasonable to add to it, but probably a mistake to try to get an element out of it.

That aligns better with the semantics of Array<unknown> instead of Array<never>.

@fatcerberus
Copy link

The problem is that if you infer unknown[] for [] then it basically becomes unassignable to anything you'd want to assign it to:

const x: number[] = [];  // error because unknown[] isn't assignable to number[]

@rotu
Copy link

rotu commented Feb 16, 2024

The problem is that if you infer unknown[] for [] then it basically becomes unassignable to anything you'd want to assign it to:

const x: number[] = [];  // error because unknown[] isn't assignable to number[]

Well, not exactly unknown[] but something that decays to it if a better type can’t be inferred. Edit: not sure if this is contextual typing or apparent typing or a free type parameter or what-have-you.

I’m modeling my expectation on how the return type of Array.of() works with no arguments. Its return type is T[] and can be assigned to an array of any type, but when initializing a variable, that variable becomes typed as unknown[].

@Andarist
Copy link
Contributor Author

I’m modeling my expectation on how the return type of Array.of() works with no arguments. Its return type is T[] and can be assigned to an array of any type, but when initializing a variable, that variable becomes typed as unknown[].

This one uses inference from a contextual return type to achieve this effect:

const a = Array.of() // unknown[]
const b: string[] = Array.of() // string[]

It is a more specific situation here since inference is involved and this example just happens to work this way. It doesn't have anything to do with arrays and array literal expressions (and their "fresh" types).

Well, not exactly unknown[] but something that decays to it if a better type can’t be inferred.

Yeah, I would imagine this being a possibility here. I understand how it would be kind of a sidestep from general rules in how all of this currently works. On the other hand, if [][number] is valid then [][0] not being allowed is already some kind of an existing exception. I'm not sure where the line is though - I feel like I've read both "arrays are not that special in the type system" and "arrays are special in the type system" on different occasions πŸ˜…

That's what noUncheckedIndexedAccess is for. When you enable it you end up with undefined, not never.

That is fine but I think that people mostly understand that this comes into play when dealing with string[] or with an explicit never[]. The surprising bit is that we get an implicit never[] here and we might end up with problems down the road because of that. I feel like those are just 2 - even if related - behaviors.

@fatcerberus
Copy link

Pinging @RyanCavanaugh back here because I'm almost positive I remember him explaining the exact reasoning for the never[] inference in the past. I feel like it probably has something to do with array types being assumed to be covariant and non-mutating, like every other bit of weirdness around array typing.

@fatcerberus
Copy link

fatcerberus commented Feb 16, 2024

Nevermind, I found his explanation: #51853 (comment)
Fun aside, contextual typing for this is apparently harder than it sounds: #51853 (comment)

@RyanCavanaugh
Copy link
Member

(reading my own comment) ahh now I get it

@rotu
Copy link

rotu commented Feb 16, 2024

@fatcerberus

For types given to inferred variables, the situation is much tricker due to evolving arrays. But object properties don't qualify for evolving arrays, so the situation is analogous:

const obj = { a: [] }
declare function doSomething(blah: { a: readonly number[] }): void
doSomething(obj);

This call is processed the same way: obj.a is never[]. This one is probably the most important to get right since there is no contextual typing to "fix" the type of [] in this example.

That can be terrifically unsound since it doesn't check whether the type is being used contravariantly. The element type of the array is bounded below by never, but the upper is not known.

const obj = { a: []}
const pushA = <T,>(blah: {a: T[]}, a:T)=>{
    blah.a.push(a)
}
pushA(obj, 4)
pushA(obj, "foo")
// obj.a is now an array containing [4, 'foo'] but typed as never[]

Then again, this does work in a covariant context: const obj = { a: [] as const } which is the common case that never[] is supposed to deal with.

@RyanCavanaugh
Copy link
Member

I have a lot of bad news about supertype-aliased writes in TypeScript

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants