-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Suggestion: should non-null assert propagate? #9640
Comments
I'm not in favor of this because of the legibility of the assertion. Unlike the leading Why not simply add a check for for (const change of changes) {
// TODO: Remove when typescript#9631 is resolved
if (change === undefined) continue;
// change is now *just* ChangeRecord unless I'm missing something
} |
@svieira Sure, it would work. But it kind of defeats the purpose of BTW what is the scenario you have in mind here? I don't think adding a If your issue is rather that you find |
The problem is with a larger code base without consistent type annotations: function assumptions(value?: string, options: OptionsType) {
// code
// code
// code
// new code
// should have been a conditional check but someone chose to do the wrong thing
// (because right now we always provide value)
const something = value!.length;
// code
// new code using something
// code
return {
// fields
details: value
// details *used* to be a string | undefined
// now, with the bad change it's a string
};
} Consider the alternatives: if (value != null) {
// new code
}
// value is still string | undefined here if (value == null) return defaultOptions;
// value is *really* a string here const nonNullValue = value as string;
// Hey, you made a cast here, very clear to see what you're doing
// If you use `value` it is still string | undefined
// Only using nonNullValue gets you the cast value The only one that is magic is non-null assertions ( const anythingElse = value!.some.property.access;
// value is now stripped of undefined even if that wasn't the intent I would rather see something like this: value = value!;
// Hey I'm really intending on mutating the type here for everything that comes after me |
Where I don't follow you is here: const anythingElse = value!.some.property.access;
// value is now stripped of undefined even if that wasn't the intent If there should have been a check and in the future |
I agree the example is not compelling as written. But consider something very similar: // in external-lib.d.ts
// File written pre-strictNullCheck; in reality accepts string | undefined
declare function doSomething(x: string): void;
// in my-file.ts
function foo(x: string | undefined): string {
// Use ! to silence error about function call that I know is actually valid
doSomething(x!);
return x;
}
let y = foo(undefined); // oops, y: string but has value 'undefined' |
@RyanCavanaugh I see. This is an usage of When I have an incomplete Nitpicker's corner: your example is broken because you explicitly say |
Re: Nitpick - correct, I'm saying that under the proposed behavior, there wouldn't be an error, which is bad. I'm not sure what you're getting at with the first 2 paragraphs there. Is there some way to meaningfully distinguish the 'bad' cases from the 'good' cases? |
@RyanCavanaugh What I meant is this: I think of When in fact your usage is different. This is really the same as this situation: // bad .d.ts, maybe because it's for an old version of a library that was not updated yet
// Actually, frob now also accepts an array: `x: number | number[]`
function frob(x: number): void;
// So in your code you do:
let some: number | number[] = [1, 2, 3];
frob(<number>some); I would not consider this good style, and to me your
Well yes: if at runtime |
Precisely because ! is a cast that removed the undefined/null types, it shouldn't behave differently than any other cast. What you're saying is not necessarily 'I know it is not undefined' but more 'I want to treat it as not undefined in this expression'. In my opinion, this suggestion would be the same as accepting something like this function foo(): number | number[] { return 3; }
function frob(x: number): void {}
let some = foo(); //some is of type 'number | number[]'
frob(some as number); //we might know it's a number, so we do this. Okay.
frob(some); //If casts "teach about" types, this would not be an error. Currently, it is. Another problem would be that, when closures are involved, a variable being 'not undefined' in one line doesn't necessarily mean that it will still be 'not undefined' in the next line. (function (){
var a: number | undefined = 3;
var b = () => { a = undefined; };
if(a) {
b();
console.log(a.toFixed());
}
})(); |
@buola this actually crossed my mind when I wrote this suggestion. I did not want to push it too far but I think that it would be perfectly logical that a cast narrows the actual type of a union when it's used, yes. I would certainly accept your example. Think about it: either the call Casting means "hey compiler I know better than you what type this variable really is in this location". Then why shouldn't the compiler learn from our hints? As you noted, your second problem is unrelated to this suggestion. It is a limitation of the type analysis as it exists today. |
Related #10421 It's not at all clear what the "scope of effect" of the assertion should be. For example It's not clear there's a good answer here. Sort of unrelatedly, the OP was caused by a compiler bug ( |
ok maybe I didn't think this as thoroughly as you guys, but couldn't it use the same rules as the data flow analysis that's already in place? In your example This is pretty much the same thing as an BTW although the idea still makes sense to me, I have been using null-strict code for a while and lack of this hasn't really been an issue. Mostly because 99% if the time my code is correctly typed. So maybe it's not worth pursuing if you feel the benefits are not worth the effort. |
I think the confusing part is that you might write code like this function fn(x: string | undefined) {
/* do something here that makes x necessarily not-null that the compiler can't detect */
// lots more lines
console.log(x!);
// lots more lines
return x.substr(3); // OK
} Then you refactor a bit function fn(x: string | undefined) {
/* do something here that makes x necessarily not-null that the compiler can't detect */
if (enableLogging) {
// lots more lines
console.log(x!);
// lots more lines
}
return x.substr(3); // Error!
} and it's really not clear why you're getting an error on the last line now, especially if the If you're not running into this much in practice, that's great news. Hopefully you're only having to use |
I agree all the inferred stuff can sometimes be a little obscure, this is already the case today. Especially when you encounter shortcomings of the typing engine. In your example, yes there is a new error. There is a good reason for that, even though I agree it can be a little obscure. Reviewing the changes (hopefully people use version control), I think it is reasonably clear that moving a type assertion (the But again I'm not particularly concerned by this anymore. If you do something in this area, I hope it's something more general, like narrowing types in a block for casts as well, etc. EDIT: The issue you linked #10421 is similar but avoids the "obscure" changes by introducing explicit syntax for block narrowing of types, maybe that's better. A no-op statement like |
Just adding a use case. This would be nice for writing real assertions: class Foo {
prop: string | null;
propLength() {
assert(this.prop!);
return this.prop.length;
}
}
function assert(value: any) {
if (!value) {
throw new AssertError("Null or undefined");
}
} This way, if I use |
@pmoleri TypeScript 3.7 introduced assertion functions exactly for this purpose, and they have clearer syntax and no need for redundant |
I was wondering whether
!
should be taken into account by the dataflow analysis, and I don't see why it shouldn't.It is like a cast that says "this value is not null", so from this point I guess the compiler could remove undefined/null from future uses.
For example, I use
!
to work around #9631, so I have code that looks like (simplified):Observe how I added 5
!
to makechange
not null.The first one could have been enough. After all, once I say "
change
is notundefined
", there is no reason to assume it could be until I modify the variable again.The text was updated successfully, but these errors were encountered: