0

I'm banging my head over this and don't get it. 'rules.optional' possibly undefined even after checking it's not. Check works normally (so case1 and case 2 are clear), but not inside array.find() which is case 3 below. I understand why it doesn't work in case 4, as it is executed asynchronously and can change in the meantime, but case 3?

const rules: {
    name: string,
    optional?: { max: number, name: string }
} = { name: "test" }
const arr: string[] = ["a", "b", "c"]

// case 1
    const obvious = rules.optional.max //rules.optional possibly undefined

// case 2
if (rules.optional) {
    const ok = rules.optional.max //OK, as expected
}

// case 3
if (rules.optional) {
    const error = arr.map(elem => elem === rules.optional.name) //rules.optional possibly undefined !?
}

// case 4
if (rules.optional) {
    setTimeout(() => { console.log(rules.optional.name) }, 1000)
}

Note that those are simplified extracts from my code. In real code, I cannot use ! (eslint complains) nor ? (code complains)

1
  • well, if I declare some const before arr.map, like const ro = rules.optional and use it inside .map() then TS picks it up. I'm still confused why this is necessary. It doesn't seem it is a very complex logic in the example above. Commented Jun 11, 2020 at 21:10

1 Answer 1

1

TypeScript's type system doesn't have any way to express what a callback-accepting function actually does with its callback. So while we know that arr.map(cb) executes cb once per array element before it returns, the compiler doesn't. For all the compiler knows, arr.map(cb) never invokes the callback, or it calls the callback asynchronously at some unspecified time in the future. For this reason, the compiler essentially resets any control-flow narrowings in the outer scope when analyzing the control flow inside the scope of the callback. Both case 3 and case 4 look the same to the compiler: maybe rules.optional will be undefined by the time the callback is called.

There is a (longstanding) open issue, microsoft/TypeScript#11498, asking to allow some way to tell the compiler that a callback-taking function will immediately invoke the function, so that any control-flow analysis form the outer scope is still valid inside the body of the callback. Such behavior would fix your problem. I don't know if it will ever happen, but you might want to go to that issue and give it a 👍 to add support for it.


As you note, by copying the narrowed rules.optional to a new variable after you've verified that it's not undefined, the compiler infers the type of that new variable as not possibly being undefined, and lets you use arr.map() on it:

if (rules.optional) {
    let ro = rules.optional;
    arr.map(elem => elem === ro.name) // okay 

That isn't because the compiler understands anything about arr.map(); it's because the compiler complains if you ever try to set ro to undefined:

    ro = undefined; // error!
}

and therefore it knows that ro.name exists wherever ro is in scope, including inside the arr.map() callback. This is a reasonable workaround when dealing with undesirable control-flow narrowing resets, at least until something like microsoft/TypeScript#11498 is introduced to the language.


Hope that helps; good luck!

Playground link to code

Sign up to request clarification or add additional context in comments.

1 Comment

thanks for a detailed answer. I was suspecting it, but wasn't sure. I had wrong expectations from Typescript, that it can know .map(), or .filter() are not async and are executed, but thinking about it more it's easy to see many possible situations when it's not the case. So only manual annotation would help. As I like to avoid annotations as much as possible, I'll stick with understanding the TS and adapting the way I write code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.