Strange scoping issue when using typeguards

前端 未结 1 1100
情书的邮戳
情书的邮戳 2021-01-24 07:24

Say we have this typescript code:

interface A { 
  bar: string;
}
const isA = (obj: T): obj is T & A => {
  obj[\'bar\'] = \'world\';
  return tr         


        
相关标签:
1条回答
  • 2021-01-24 08:09

    The general answer is that you are cleverer than the compiler in this case. TypeScript uses heuristics to analyze the control flow of the code to try to infer narrower types for expressions. It does a reasonable job of this, but it isn't perfect and probably can never be.

    When you access obj.bar immediately after throwing if isA(obj) returns false, the compiler narrows obj to include A, as you expect. Unfortunately, when you create a closure and pass it to Array.prototype.forEach(), the compiler widens obj back to its original type which does not include A. Now you and I know that forEach() will immediately invoke its callback function, but TypeScript doesn't. For all it knows, the value of obj will be modified before the callback is invoked. And there's no way to tell the compiler differently. So it decides that narrowing isn't safe and gives up.

    So, workarounds: one idea is to make obj a const instead of declaring it with let:

    const obj = { foo: 'hello' };
    if (!isA(obj)) throw 'wont ever throw'
    Array(5).fill(0).forEach((_, i) => {
      obj.bar // This is okay now
    });
    

    This doesn't actually change the fact that obj.bar can be added or removed before a callback that closes over obj is called, but the heuristic that TypeScript uses is something like "const is more likely to be immutable than let", even though it's not, really.

    A similar workaround, if you can't make obj a const, is to assign a new const variable after the narrowing takes place, and use that in the callback instead:

    let obj = { foo: 'hello' };
    if (!isA(obj)) throw 'wont ever throw'
    const myObj = obj;    
    Array(5).fill(0).forEach((_, i) => {
      myObj.bar // This is okay
    });
    

    Of course by that token you might as well skip the obj middleman entirely:

    let obj = { foo: 'hello' };
    if (!isA(obj)) throw 'wont ever throw'
    const bar = obj.bar;    
    Array(5).fill(0).forEach((_, i) => {
      bar // This is okay
    });
    

    It's up to you. Hope that helps. Good luck!

    0 讨论(0)
提交回复
热议问题