-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Type inference/narrowing lost after assignment #27706
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
Comments
@DanielRosenwasser We do not currently narrow non-union types on assignment. For example, given declare class Foo {
x: string;
}
declare class Bar extends Foo {
y: string;
}
declare var x: Foo;
if (x instanceof Bar) {
x.y;
x = new Bar();
x.y;
} we error on the second Did we want to change this? |
👍 Can we expect this in 3.3.0, or no guarantees yet? Here's my use case. |
Another related simple example: function reverse1(a: ArrayLike<number>): number[] {
return Array.from(a).reverse(); // works!
}
function reverse2(a: ArrayLike<number>): number[] {
a = Array.from(a); // makes "a" a real Array
return a.reverse(); // error; TS thinks "a" is still of type ArrayLike
} |
Another example, which to me is quite unintuitive, is with objects that contain union typed properties: // compiles
let myVar: string | number;
myVar = '5';
console.log(myVar.length);
// compiles
let myObj1: {myProp: string | number} = {myProp: 5};
myObj1.myProp = '5';
console.log(myObj1.myProp.length);
// does not compile, emitting the following error:
// TSError: ⨯ Unable to compile TypeScript:
// myt.ts(63,26): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
let myObj2: {myProp: string | number} = {myProp: 5};
myObj2 = {myProp: '5'};
console.log(myObj2.myProp.length); |
I just stumbled into this aswell, albeit with number instead of string. I can't see any logical reason why assigning a declare function takesNumber(value: number): void;
function test(value: unknown) {
if (typeof value !== "number") return;
takesNumber(value); // ok
value = 1; // assign the same type!
takesNumber(value); // error (value is unknown, although we narrowed it to number before)
} I would really love to have an |
According to #45870 (comment), "That an This issue has admittedly been painful for us while porting the TC39 Temporal polyfill from JS to TS. The original JS polyfill does a lot of mutation of parameters, usually for type coercion. For example, a function may take a parameter which can either be a string or an object, and if it's an object then it's coerced to a string in the first line of the function. Later code knows that it's always a string, like old-school JS's runtime equivalent of a TS type guard function. To port this kind of code to TS, we have many bad alternatives:
For a real-world example, today I screwed up a bisection refactor. Here's original JS: function TotalDurationNanoseconds(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, offsetShift ) {
if (days !== 0) nanoseconds = bigInt(nanoseconds).subtract(offsetShift);
hours = bigInt(hours).add(bigInt(days).multiply(24));
minutes = bigInt(minutes).add(hours.multiply(60));
seconds = bigInt(seconds).add(minutes.multiply(60));
milliseconds = bigInt(milliseconds).add(seconds.multiply(1000));
microseconds = bigInt(microseconds).add(milliseconds.multiply(1000));
return bigInt(nanoseconds).add(microseconds.multiply(1000));
} If assignments worked like type guard functions, then AFAICT I wouldn't have had to make any changes to the body of the JS function at all. Below is my naive expectation of how types should change after each assignments. Note that the big-integer functions accept either function TotalDurationNanoseconds(days: number, hours: number, minutes: number, seconds: number, milliseconds: number, microseconds: number, nanoseconds: number, offsetShift: number) {
if (days !== 0) nanoseconds = bigInt(nanoseconds).subtract(offsetShift);
// => nanoseconds: number | bigInt.BigInteger
hours = bigInt(hours).add(bigInt(days).multiply(24));
// => hours: bigInt.BigInteger
minutes = bigInt(minutes).add(hours.multiply(60));
// => minutes: bigInt.BigInteger
seconds = bigInt(seconds).add(minutes.multiply(60));
// => seconds: bigInt.BigInteger
milliseconds = bigInt(milliseconds).add(seconds.multiply(1000));
// => milliseconds: bigInt.BigInteger
microseconds = bigInt(microseconds).add(milliseconds.multiply(1000));
// => microseconds: bigInt.BigInteger
return bigInt(nanoseconds).add(microseconds.multiply(1000));
// => return type: bigInt.BigInteger
} But because assignment doesn't act like type guards, I had to do a non-trivial refactor:
Here's my buggy initial port. Keep in mind this was just one of hundreds I ported that day. 😄 export function TotalDurationNanoseconds(
daysParam: number,
hoursParam: number,
minutesParam: number,
secondsParam: number,
millisecondsParam: number,
microsecondsParam: number,
nanosecondsParam: number,
offsetShift: number
) {
let days: bigInt.BigInteger, nanoseconds: bigInt.BigInteger;
if (daysParam !== 0) nanoseconds = bigInt(nanosecondsParam).subtract(offsetShift);
let hours = bigInt(hoursParam).add(bigInt(days).multiply(24));
let minutes = bigInt(minutesParam).add(hours.multiply(60));
let seconds = bigInt(secondsParam).add(minutes.multiply(60));
let milliseconds = bigInt(millisecondsParam).add(seconds.multiply(1000));
let microseconds = bigInt(microsecondsParam).add(milliseconds.multiply(1000));
return bigInt(nanoseconds).add(microseconds.multiply(1000));
} The bug is forgetting to initialize Why can't assignment act like a type guard function? Is the current behavior designed this way because it has big benefits in some use cases? Or is it a technical limitation, e.g. types can be narrowed but not expanded/changed? cc @12wrigja |
I also ran into this issue today while trying to use For arrays in Javascript this is particularly noticeable if you have broken down multiple method calls onto separate lines using reassignments, you just can't do it if the variable was originally of type unknown. |
Any news on the subject guys? 😶🌫️ The number of duplicates seems to indicate that there is a real need for this feature 👀 Thanks :) |
Just came to report this myself. Search terms "type narrowing lost after assignment". Kudos to OP for naming the issue perfectly. |
Worth noting this also happens with |
TypeScript Version: 3.1
Search Terms: type inference, type guard, narrowing, lost, assignment
Code
Expected behavior: This should compile without an error.
Actual behavior: Line 7 fails with:
Object is of type 'unknown'.
Playground Link: https://www.typescriptlang.org/play/index.html#src=let%20a%3A%20unknown%20%3D%20'x'%3B%0D%0A%0D%0Aif%20(typeof%20a%20%3D%3D%3D%20'string')%20%7B%0D%0A%20%20%2F%2F%20a%20inferred%20as%20%60string%60%0D%0A%20%20a%20%3D%20a.substr(0%2C%205)%3B%0D%0A%20%20%2F%2F%20a%20inferred%20as%20%60unknown%60%0D%0A%20%20a.length%3B%0D%0A%7D%0D%0A
Related Issues: #18840, #19955, #26673
The text was updated successfully, but these errors were encountered: