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

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

Closed
ivogabe opened this issue May 28, 2016 · 10 comments · Fixed by #8944
Closed

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

ivogabe opened this issue May 28, 2016 · 10 comments · Fixed by #8944
Assignees
Labels
Bug A bug in TypeScript

Comments

@ivogabe
Copy link
Contributor

ivogabe commented May 28, 2016

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.

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

@kitsonk
Copy link
Contributor

kitsonk commented May 29, 2016

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.

@ivogabe
Copy link
Contributor Author

ivogabe commented May 29, 2016

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([]);

@Arnavion
Copy link
Contributor

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

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

@ivogabe
Copy link
Contributor Author

ivogabe commented May 29, 2016

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

@Arnavion
Copy link
Contributor

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.

@ivogabe
Copy link
Contributor Author

ivogabe commented May 29, 2016

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

@kitsonk
Copy link
Contributor

kitsonk commented May 30, 2016

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.

@ivogabe
Copy link
Contributor Author

ivogabe commented May 30, 2016

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"]);

@ahejlsberg
Copy link
Member

ahejlsberg commented May 30, 2016

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

@ahejlsberg
Copy link
Member

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

@mhegazy mhegazy added the Bug A bug in TypeScript label Jun 1, 2016
@mhegazy mhegazy added this to the TypeScript 2.0 milestone Jun 1, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants