最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - Why these two seemingly identical functions have different narrowing effects? - Stack Overflow

programmeradmin0浏览0评论

These two functions should have the same type ((msg?:string)=>never) and should be functionally identical:

const fail1 = (msg?:string)=>{ throw new Error(msg); }
const fail2: (msg?:string)=>never = fail1;

Yet, fail1 fails to narrow variables, while fail2 succeeds:

declare const x: string | undefined

x.toUpperCase(); // error (as expected): 'x' is possibly 'undefined'.

if (!x) fail1();
x.toUpperCase(); // error: 'x' is possibly 'undefined'.

if (!x) fail2();
x.toUpperCase(); // OK

TypeScript is able to deduce that x can no longer be undefined after the if (!x) fail2(); statement. However it's not able to deduce the same after if (!x) fail1();.

Why?

You can play with my code on the TypeScript Playground.

These two functions should have the same type ((msg?:string)=>never) and should be functionally identical:

const fail1 = (msg?:string)=>{ throw new Error(msg); }
const fail2: (msg?:string)=>never = fail1;

Yet, fail1 fails to narrow variables, while fail2 succeeds:

declare const x: string | undefined

x.toUpperCase(); // error (as expected): 'x' is possibly 'undefined'.

if (!x) fail1();
x.toUpperCase(); // error: 'x' is possibly 'undefined'.

if (!x) fail2();
x.toUpperCase(); // OK

TypeScript is able to deduce that x can no longer be undefined after the if (!x) fail2(); statement. However it's not able to deduce the same after if (!x) fail1();.

Why?

You can play with my code on the TypeScript Playground.

Share Improve this question asked Jan 19 at 14:25 Blue NebulaBlue Nebula 1,1027 silver badges13 bronze badges 5
  • If I insert const blah: never = fail1; and const blah2: never = fail2;, I get error messages Type '(msg?: string) => never' is not assignable to type 'never'. and Type '(msg?: string | undefined) => never' is not assignable to type 'never'.. So it looks like TypeScript doesn't think fail1 and fail2 have the same type, but I don't know why that would affect anything. – user2357112 Commented Jan 19 at 14:52
  • If I adjust the definition of fail1 to say const fail1 = (msg?:string|undefined)=>{ throw new Error(msg); }, the type discrepancy seems to go away, but the narrowing difference remains. So this might not be related to the problem. – user2357112 Commented Jan 19 at 14:57
  • That's very interesting and surprising. When I check the type of those functions (for example by hovering them in the playground), I see the same one: (msg?: string) => never. I don't see how the input parameters' type could affect narrowing in this case. But your findings may suggest that there may be hidden differences in the output type as well? – Blue Nebula Commented Jan 19 at 14:59
  • If I remove the msg argument entirely, the discrepancy still remains. It looks like the explicit type on fail2 is the key. Declaring an explicit type for fail1 lets it narrow, and const fail3 = fail2; doesn't narrow. I still don't know why this would matter, though. – user2357112 Commented Jan 19 at 15:03
  • Yes, I noticed the same. It seems like an explicit never return type is somehow different from an inferred one? – Blue Nebula Commented Jan 19 at 15:04
Add a comment  | 

1 Answer 1

Reset to default 2

Support for control flow analysis of never-returning functions as introduced with TypeScript 3.7 was implemented in microsoft/TypeScript#32695. In the description of that pull request, it says:

A function call is analyzed as an assertion call or never-returning call when

  • the call occurs as a top-level expression statement, and
  • the call specifies a single identifier or a dotted sequence of identifiers for the function name, and
  • each identifier in the function name references an entity with an explicit type, and
  • the function name resolves to a function type with an asserts return type or an explicit never return type annotation.

An entity is considered to have an explicit type when it is declared as a function, method, class or namespace, or as a variable, parameter or property with an explicit type annotation. (This particular rule exists so that control flow analysis of potential assertion calls doesn't circularly trigger further analysis.)

So fail1 just has an inferred never return type, while fail2 has an explicitly annotated never return type. The reason why this matters for control flow analysis is to avoid potentially catastrophic performance problems. (See this comment on related issue microsoft/TypeScript#45385 for an authoritative source.)

If there were no such restriction, then the type checker would need to analyze the body of a function like fail1 in order to determine if calls to it would narrow. This would depend on the results of control flow analysis inside the body of the function. There might easily be other function calls inside that function, any of which might not be explicitly annotated as well. So those function calls would trigger control flow analysis of their function bodies. This could very easily cause a cascade of analysis which either takes a very long time to complete or simply fails to complete because of circularity. To avert this sort of thing, a line needs to be drawn somewhere to say that analysis will not cross that line. They decided to put the line at the explicitness of the type of the function itself.

You might wish that they had drawn the line to allow for some kind of "shallow" control flow analysis of the function body, so that as long as the body of fail1 didn't do "too much", the analysis would be allowed. Then presumably the fact that it immediately throws would allow the function to be treated as an assertion of never. But then an alternate universe version of this question would be asked where one function immediately throws, and another does something like sayHello(); throw new Error(msg); and why does a call to the first function narrow while the call to the second function doesn't? And then when considering explaining the shallow analysis, the alternate universe question asker would wonder why the line hadn't been drawn slightly deeper.

In order to keep things tractable and somewhat sane, they made the decision where they did because it depends directly on type annotations which are discoverable in code, rather than having some harder-to-find analysis depth limit.

No, it's not great. It's caused a lot of confusion. Some of this confusion could be cleared up by more explicit documentation, but even with it, it's not obvious what's going on. Some days I think maybe this entire feature should have been withheld rather than released with such restrictive rules. Presumably alternate-universe me would complain about its absence. In this universe, the answer to your question is located at microsoft/TypeScript#32695.

发布评论

评论列表(0)

  1. 暂无评论