Skip to content

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

Closed

Description

๐Ÿ”Ž 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

Activity

  1. MartinJohns commented on Feb 15, 2024

    @MartinJohns
    Contributor

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

  2. RyanCavanaugh commented on Feb 15, 2024

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

  3. rotu commented on Feb 16, 2024

    @rotu

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

  4. fatcerberus commented on Feb 16, 2024

    @fatcerberus

    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[]
  5. rotu commented on Feb 16, 2024

    @rotu

    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[].

  6. Andarist commented on Feb 16, 2024

    @Andarist
    ContributorAuthor

    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.

  7. fatcerberus commented on Feb 16, 2024

    @fatcerberus

    Pinging Ryan Cavanaugh (@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.

  8. fatcerberus commented on Feb 16, 2024

    @fatcerberus

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

  9. RyanCavanaugh commented on Feb 16, 2024

    @RyanCavanaugh
    Member

    (reading my own comment) ahh now I get it

  10. rotu commented on Feb 16, 2024

    @rotu

    Bruce Pascoe (@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.

  11. RyanCavanaugh commented on Feb 16, 2024

    @RyanCavanaugh
    Member

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

  12. 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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions