0

I have the following code:

class Blog {
    private _id?: number

    private async create() {
        const id = await getIdOfBlog()
        this._id = id
    }

    async track() {
        await this.create()
        doSomethingWithId(this._id)
    }
}

function doSomethingWithId(id: number) {
    
}

TS will complain:

Argument of type 'number | undefined' is not assignable to parameter of type 'number'.
  Type 'undefined' is not assignable to type 'number'.

I understand why it happens. TS thinks this._id could be undefined, although I know more, I have context that this.create() will fill the variable. Is it possible to circuvein the type checking without using @ts-ignore?

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
user14288887
  • 55
  • 1
  • 4

2 Answers2

2

_id is marked as optional, TS warns about this and it is right. The code logic is probably flawed and you attempt to fool TS instead of fixing the logic.

If _id must be optional then either doSomethingWithId() should allow undefined as argument or track() must call doSomethingWithId() only if it is sure that _id is set.

On the other hand, _id is an identifier and an identifier is never undefined. It represents the identity of the object; it cannot be undefined and it must be set when the object is created.

Your class does not have a constructor and this makes its instances just regular objects with a fancy prototype. The purpose of a class is to enforce the creation and usage of entities that are always consistent. The code const x = new Blog() produces an empty object. It's _id property is undefined (_id is not even present in x).

This is how it can be implemented using OOP at its full potential and not just as a buzzword:

class Blog {
    public constructor(private id: number) { }

    async track() {
        doSomethingWithId(this.id)
    }
}

And this is how it is used:

const id = await getIdOfBlog()
const x = new Blog(id)

Now x is a valid object. It has a value in its identity property.

This approach has other advantages too. I guess getIdOfBlog() gets the value from a database or another external resources. In the tests of class Blog you don't want to wait for the code that accesses external resources; you don't even want to run such code in tests.
In the tests of class Blog you can simply create an instance of the class as new Blog(42) then use it.

axiac
  • 68,258
  • 9
  • 99
  • 134
1

I usually handle this by adding a test that throws an error:

async track() {
    await this.create()
    if (!this._id) throw new Error("Blog does not have an id")
    doSomethingWithId(this._id)
}

This way typescript knows that if the control flow is on the last line, then this._id must have a truthy value.

Playground


Additionally, you can use the ! postfix operator, which means:

"I, the programmer, am asserting that this nullable type is actually guaranteed not to be null or undefined in this scope."

async track() {
    await this.create()
    doSomethingWithId(this._id!)
}

Usually though, this is discouraged in all but the absolute simplest of cases. If you are wrong (or there is a bug elsewhere) you'll get some strange error messages in other functions that will be harder to track down.

So if you opt for this method, and for some reason it actually is undefined, then you probably throw an error either way, but this way the error message will be far less helpful.

Playground

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337