Home > Software design >  Typescript: check for undefined does not work within an inside function
Typescript: check for undefined does not work within an inside function

Time:02-05

Look at the code below. Look at test getter. Why does not const name = this.person.name throw an error while const processPerson = () => this.person.name does throw an error?

interface Person { name: string; age: number }

class TestClass {
    get person(): Person | undefined {
        if (Math.random() > 0.5) return undefined
        return { name: 'Bob', age: 35 }
    }

    get test() {
        if (!this.person) return undefined
        const name = this.person.name // No error
        const processPerson = () => this.person.name // Object is possibly 'undefined'.(2532)
        return processPerson()
    }
}

CodePudding user response:

Problem

you need to understand that in a function there will be a new scope imagine if you keep the same code and return processPerson instead of processPerson() and run this code

const test = new TestClass()

test.person = { name: '', age: 0 }

const fn = test.test()

fn() // is ''
test.person = undefined
fn() // error because this.person is undefined

yes that means the this.person in processPerson can perfectly is undefined since it doesn't need to pass the this.person check

Resolve

you have 2 workarounds:

  • if you want processPerson function to return realtime value of this.person.name you must accept this.person check in function

  • otherwise you can do this

class TestClass {
  get test() {
    const { person } = this
    if (!person) return undefined

    const name = person.name
    const processPerson = () => name

    return processPerson()
  }
}

CodePudding user response:

The problem is that your test calls the getter twice, and since the getter randomly returns either undefined or a value, the two lines don't get the same value.

Looks like the first call to the getter gets a value, while the second call gets undefined.

To fix this, call the getter once at the beginning of the test method, and assign the result to a local variable. Then test the local variable instead of calling the getter, like this

test() {
        const subject = this.person;
        if (!subject) return undefined
        const name = subject.name
        const processPerson = () => subject
        return processPerson()
    }

CodePudding user response:

This behaviour can happen when you don't bind this, because when defining a new function, it will have it's own this, which will override/ clash with the class.

In order for this to work, you shouldn't use arrow functions, since they don't have an internal this pointer.

If you want to use this inside processPerson, run:

return processPerson.bind(this)

Alternatively you could store the value of this in another variable name, however I see it as a less adequate solution to inherit some properties, but it can be deep copied with Object.assign or something equivalent.

The final snippet will be something like this:

interface Person { name: string; age: number }

class TestClass {
    get person(): Person | undefined {
        if (Math.random() > 0.5) return undefined
        return { name: 'Bob', age: 35 }
    }

    get test() {
        if (!this.person) return undefined
        const name = this.person.name // No error
        const processPerson = (function() {
            return this.person.name;
        }).bind(this);
        return processPerson()
    }
}

This guard check: if (!this.person) return undefined will prevent that this.person is undefined when we create the returned function, but won't prevent it to be undefined when the function is called and runs, and since the returned function's this is not freezed when it's generated, it'll be undefined behaviour (non deterministic, meaning you can't trust the value). The same behaviour could be introduced in a function that uses global variables, so in order to keep this encapsulated, it's good to bind it.

CodePudding user response:

Let me try to explain you what is typescript thinks about your code and how it acts:


class TestClass {
   // this is the worst possible getter for a typescript compiler
   // the random nature of the getter deceives both the compiler and the developer
    get person(): Person | undefined {
        if (Math.random() > 0.5) return undefined
        return { name: 'Bob', age: 35 }
    }

    get test() {
        if (!this.person) return undefined
        // In here TS compiler thinks that accessing this.person.name 
        // is safe because the above line has a guard for it.
        // But it will throw runtime errors by P chance
        // Note that TS doesn't aware of its randomness here
        const name = this.person.name //
        // If we describe this line we call it a local arrow function definition
        // Since it's a definition and also there is no nullability guard in it TS is right about complaining here
        // Since it's a definition it is not deterministic where it 
        // will be called in the code, and the compiler can't be sure whether it is null or not.
        const processPerson = () => this.person.name // Object is possibly 'undefined'.(2532)
        return processPerson()
    }
}

The other answers have already good practices to avoid the error and possible runtime issues.

  • Related