Ask
61
@headless_hugo ·

narrowing disappears inside a callback even though i checked for null two lines up Narrowing

if (!user.email) return
setTimeout(() => {
  send(user.email)
  //   ^ Argument of type 'string | undefined' is not
  //     assignable to parameter of type 'string'
}, 100)

The check is right there. Nothing between them touches user. What confuses me more is that the same pattern inside .map() sometimes compiles fine and sometimes does not, so I clearly do not understand the rule at all.

9 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @deadbug_wiring · 3h ago

    Design fix rather than a type fix: make the function take what it needs. send(email: string) instead of send(user) reaching three levels into an object. Roughly half the narrowing pain I review comes from functions that accept a big object and then have to defend against every optional field on it.

    29
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @wellhead_wanda · yesterday · 3 replies

    Copy it into a local const and the problem goes away:

    const email = user.email
    if (!email) return
    setTimeout(() => send(email), 100)
    

    The rule is about what the compiler can prove. It keeps narrowing on a const binding inside a closure because a const cannot be reassigned, ever, so the fact you established is still true whenever the closure runs. It cannot prove anything about user.email — that is a mutable property on an object anyone holds a reference to, and between now and 100ms from now literally any code could set it to undefined, including send itself. So it throws the narrowing away at the function boundary.

    The reason .map() looks inconsistent is not that map is special. In the cases that compiled you were narrowing a local const; in the cases that did not you were narrowing a property. Same rule, different subject.

    47
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @orm_tamsin · 2d ago

      Same reason if (this.x) never survives into a callback in class code, which is where most people meet this. Destructuring the fields you need at the top of the method makes an entire category of this disappear.

      14
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @headless_hugo · 21h ago

      const versus property, not sync versus async. I had built an entirely wrong mental model around timing and it explains three other places this has bitten me.

      18
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @orm_tamsin · 3h ago · 2 replies

    If you find yourself doing the const dance twenty times, lift it into one place:

    function withEmail(u: User): (User & { email: string }) | null {
      return u.email ? (u as User & { email: string }) : null
    }
    

    One cast, in one file, reviewed once, and everything downstream is honest about what it has.

    21
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @boundaries_bo · 11h ago

      Or model it so the impossible state cannot be represented — a discriminated union between a verified user and an unverified one. The cast is a patch on a data model that says an email might not be there when your code clearly believes it is.

      11
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @rest_day_rita · 2d ago

    TypeScript is not being awkward here, it is being correct in an annoying way. setTimeout runs in 100ms. A lot can happen in 100ms.

    12
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @thermal_theo · 20h ago · 2 replies

    user.email! — that is exactly what the non-null assertion is for.

    8
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @wellhead_wanda · yesterday

      It is what it is for when you know something the compiler cannot. Here the compiler is right and you are one line away from proving it. Using ! in this spot is how you end up reading a 3am stack trace about undefined.

      7
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report