Skip to content

Infer bottom type for type argument when passing empty array #8878

Closed

Description

I think the bottom type, never, could be used for this case: when passing an empty array to an argument of type T[], where T is a type argument.

Anders Hejlsberg (@ahejlsberg) What do you think?

TypeScript Version:

1.7.5 / 1.8.0-beta / nightly (1.9.0-dev.20160528-1.0)

Code

function a<T>(xs: T[]) {
    return xs;
}
const x = a([]);
function b<T extends Node>(xs: T[]) {
    return xs;
}
const y = b([]);

Expected behavior:
Type of x would be never[], y would be Node[]

Actual behavior:
Type of x is any[], y is any[].

Activity

  1. kitsonk commented on May 29, 2016

    @kitsonk
    Contributor

    Wouldn't any be better, with an error if noImplicitAny is used?

    There was also a lot of discussion of why {} gets inferred for generics. Any change here would cause all sorts of carnage.

  2. ivogabe commented on May 29, 2016

    @ivogabe
    ContributorAuthor

    any is sound here, but the inference should ideally find the most specific type that is sound. any is the least specific type (top type). never is more specific than every other type (bottom type). In the body of these functions, there will never be a value with the type of the type argument, so never is a sound type for the type argument.

    never is very useful when used with union, as it then just disappears. Taking a union with any resolves in any and quickly everything is any. In this example, z is currently inferred as any but with this change, it would be undefined ( = never | undefined).

    function first<T>(xs: T[]): T | undefined {
        return xs[0];
    }
    const z = first([]);
  3. Arnavion commented on May 29, 2016

    @Arnavion
    var x = [];
    x.push(5);

    needs to continue to work, so x needs to continue to be any[] in this case.

  4. ivogabe commented on May 29, 2016

    @ivogabe
    ContributorAuthor

    Arnav Singh (@Arnavion) Agree, but I only wanted this for values of a parameter with a type argument.

  5. Arnavion commented on May 29, 2016

    @Arnavion

    I thought the value of the generic parameter is derived from the type of the expression, so the fact that [] is any[] is used to refine T to any. Even if this isn't actually how the typechecker/binder actually works, it's a simple mental model for TS users. It would be confusing for [] to be inferred as any[] when assigning to a variable and never[] when assigning to a function parameter.

  6. ivogabe commented on May 29, 2016

    @ivogabe
    ContributorAuthor

    I thought the value of the generic parameter is derived from the type of the expression, so the fact that [] is any[] is used to refine T to any.

    In general, that's true. Though with contextual typing, these types of the arguments will be inferred differently. For instance, the type of [1, ""] is (number | string)[]. But with contextual typing this could be inferred as a tuple type, [number, string]. For instance when foo<T, V>(a: [T, V]) is called with foo([1, ""]). I think that contextual typing could be used to infer the type of [] to never[].

  7. kitsonk commented on May 30, 2016

    @kitsonk
    Contributor

    think that contextual typing could be used to infer the type of [] to never[].

    It could, but I can't see that being the intent in most cases. Almost every contextual type is as wide as can be inferred. You are suggesting this one of case (because you have a further widening challenge) to totally narrow down this one case to nothing. So far every use of never is when it is clear that all other types have been eliminated and the type can be no-longer narrowed. That would never be the case with an [] unless of core is it a frozen empty array.

  8. ivogabe commented on May 30, 2016

    @ivogabe
    ContributorAuthor

    The type never means that that code is not reachable. Currently that happens for instance with type guards (typeof x === "string" && typeof x === "number") and with a function that always throws (function a() { throw new Error(); }). When you pass [] to a function, and that argument is the only reference to a type argument, the body of that function can never have a value of that type. Thus that type is 'unreachable' in the body and never is thus valid here.

    I also found out that the following code doesn't work as expected. y is inferred as (string | undefined)[], whereas it should be string[]. I think that this is related to this issue as well.

    function concat<T>(xs: T[], ys: T[]): T[] {
        return [...xs, ...ys];
    }
    const y = concat([], ["a"]);
  9. ahejlsberg commented on May 30, 2016

    @ahejlsberg
    Member

    Starting with your last example, it is definitely a bug that y is inferred as (string | undefined)[] in this example:

    function concat<T>(xs: T[], ys: T[]): T[] {
        return [...xs, ...ys];
    }
    const y = concat([], ["a"]);

    It should be string[]. The issue only shows up with --strictNullChecks and has to do with the fact that we still infer undefined[] for an empty array literal. We need to infer a true bottom type, so in strict null checking mode we should infer never[].

    Now, regarding the original proposal to infer never[] for this example:

    function a<T>(xs: T[]) {
        return xs;
    }
    const x = a([]);  // Could we infer never[]?

    We actually do infer never[] for the array literal, but then we widen during type argument inference in the call a([]) so we end up with any[]. There was a proposal not to do this in #1436. The core issue is that we need to modify the widening algorithm to handle circularities and that is not trivial to do. See my comment here.

    However, even if we stopped widening in type inference, it isn't clear that we shouldn't widen when we're inferring a type for x above. It would be reasonable to keep the never[] type only in situations where the type is deeply immutable (a concept we don't currently have), but a const is only shallowly immutable. As Arnav Singh (@Arnavion) points out, it would be odd if the following didn't work:

    const a = [];
    a.push(5);  // Don't want an error here

    Contextual typing was also mentioned in the thread above, but that really doesn't have anything to do with widening.

  10. ahejlsberg commented on May 30, 2016

    @ahejlsberg
    Member

    I did some edits to the comment above to fix some inaccuracies. It should be good now.

  11. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions