Search code examples
typescriptfunctionundefined

TS2722: Cannot invoke an object which is possibly 'undefined' on a well-defined & readonly function


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'

  • the code clearly defines the john.getName function
  • since getName 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?


Solution

  • 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.

    Playground link to code