I just stumbled upon this:
type Author = {
readonly getName?: () => string;
}
const john: Author = {
getName: () => 'John Doe',
};
john.getName(); // TS2722: Cannot invoke an object which is possibly 'undefined'
john.getName
functiongetName
is readonly, it cannot be modified & set to undefined
but TypeScript doesn't seem to agree.
I know I could use one of these:
john.getName!();
john.getName?.();
john.getName && john.getName();
but I have no clue to why this would be needed in this case.
Is there a way to avoid those ugly workarounds?
When you annotate with a (non-union) type, you've essentially told the compiler to ignore any more specific information it might be able to infer from the initializing value. In
type Author = {
readonly getName?: () => string;
}
const john: Author = {
getName: () => 'John Doe',
};
The compiler only knows that john
is of type Author
, and therefore its getName
property is optional and might be undefined
:
john.getName(); // error
One approach therefore is to leave off the annotation and let the compiler infer the type of the variable from the initializer:
const john = {
getName: () => 'John Doe',
};
john.getName(); // okay
This doesn't ensure that john
is compatible with Author
, though, which is presumably why you annotated it as such originally:
const john = {
getName: () => 123, // <-- no error
}
You can rectify this by using the satisfies
operator instead of an annotation, so you're warned about any mismatches:
const john = {
getName: () => 123, // <-- error!
//~~~~~ <-- Type 'number' is not assignable to type 'string'
} satisfies Author
and you can fix them:
const john = {
getName: () => 'John Doe',
} satisfies Author // okay
Note that I specified that your problem happens when you annotate with a non-union type. If you use a union type instead, then assignment to the variable will narrow the apparent type of the variable. If Author
had instead been
type Author = {
readonly getName: () => string;
} | {
readonly getName?: never;
}
Then the following assignment
const john: Author = {
getName: () => 'John Doe',
};
would give you the narrowing you presumably expected to happen in the first place:
john;
// ^? const john: { readonly getName: () => string; }
john.getName(); // okay
But such an approach relies on you refactoring your types so that they are unions, which has observable effects:
const w: { a: 1 | 2 } = { a: 1 }
w.a = 2; // okay
const x: { a: 1 } | { a: 2 } = { a: 1 }
x.a = 2; // error
So it might not be appropriate for every use case.