1

Okay, I'm stumped here. I have a simple React component (just starting out). I need to fetch the data so I'm waiting on some async data. This is my solution as it stands now:

import { Component } from "react";
import { Bible } from "./models";

interface State {
  bible?: Bible;
}

export class RevApp extends Component<{}, State> {
  state = {
    bible: undefined,
  };

  componentDidMount() {
    Bible.onReady().then((bible) => {
      this.setState({
        bible,
      });
    });
  }

  render() {
    const { bible } = this.state;
    //return <div>{bible ? bible.ls() : "Loading Bible..."}</div>;
    if (bible) {
      return <div>{bible.ls()}</div>;
    } else {
      return <div>Loading Bible...</div>;
    }
  }
}

For some reason I get for the bible.ls() (specifically the bible object) the compiler error "Object is possibly undefined". This makes no sense! I have tried putting an ! (i.e. bible!.ls() but then I get a complaint that "Property 'ls' does not exist on type 'never'".

The real head scratcher is that this error only shows up in my linter, but not my compiler. Which is all well and good except when I push to production my code needs to pass the linter before it is compiled.

Can anyone tell me why I am getting this behavior?

Here is my tsconfig in case it has something to do with the issue (generated by nextjs):

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "baseUrl": "."
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

typescript version "4.3.5"

node version "14.17.3"

Jim Hessin
  • 137
  • 2
  • 11
  • Does it get fixed if you annotate the `state` property explicitly like `state: State = { bible: undefined };`? It would be very helpful if you could produce a [mcve] suitable for dropping into a standalone IDE like [The TypeScript Playground](https://tsplay.dev/mLRyZw), where the only issues present are the ones you're seeing. – jcalz Aug 03 '21 at 02:24

3 Answers3

1

For better or worse, when you have a class that extends a superclass, overridden properties of the subclass do not inherit their types from the superclass. They instead get types inferred entirely from the initializer, as if the superclass were not present. So the subclass property could easily be a narrower type than that of the superclass. See microsoft/TypeScript#10570 for a discussion of this issue, how it confuses people, and how they haven't figured out anything better to do here.

That means the state property of RevApp does not inherit the type of Component<{}, State>['state']. Instead, it is inferred from the {bible: undefined} initializer, which results in the type {bible: undefined}. So the bible property of the state property of RevApp is always and forever undefined:

export class RevApp extends Component<{}, State> {
  state = {
    bible: undefined,
  };
  /* RevApp.state: { bible: undefined;} <-- oops */
}

And so you cannot possibly eliminate undefined from the type of state by type guarding (well, you could, and maybe the compiler would narrow to never, but that's not what you want either); it stays undefined:

const { bible } = this.state;
// const bible: undefined
if (bible) {
  bible // <-- still undefined
}

Presumably you actually want state's bible property to maybe not be undefined. If so, the only way to get this behavior is to annotate it explicitly as the type you want it to be:

export class RevApp extends Component<{}, State> { state: State = { // annotated bible: undefined, }; }

And now the type guard works as desired; the type of bible is Bible | undefined before the check, and just Bible if it is truthy:

render() {
  const { bible } = this.state;
  return <div>{bible ? bible.ls() : "Loading Bible..."}</div>; // okay
}

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
0
interface State {
  bible?: Bible;
}

It's because you told it that it can be undefined so typescript is picking up that it could possibly be undefined.

interface State {
  bible: Bible;
}

Remove the "?" if you don't want typescript to complain about it. Alternatively if it actually can be undefined:

state = {
  bible: undefined,
};

Then you need to make sure you check it before using it:

return <div>{bible && bible.ls()}</div>

Something else that's a bit weird here is that you're importing Bible from './models' and then you're using it as a type and also with a method (onReady), I'm not sure that 'Bible' can be both a type and a class at the same time. You're probably using the type incorrectly and need to import another type for that model.

0

The problem here is that State doesn't need initialized if it can be undefined. So removing this line:

state = {
    bible: undefined,
  };

Solves the problem - which was RE-defining the bible as strictly undefined instead of Bible?!

Jim Hessin
  • 137
  • 2
  • 11