Home > Back-end >  TS18048: variable is possibly undefined although being checked
TS18048: variable is possibly undefined although being checked

Time:01-21

interface Result {
  a: Record<string, string>;
  b: string;
  c: string
}

const func = async (): Promise < Result > => {
  // some logic
  return {
    a: {
        aa: 'aa',
      bb: 'bb',
      cc: 'cc'
    },
    b: 'b',
    c: 'c'
  };
}

const func2 = async (val: string): Promise < string > => {
  // some logic
  return `${val}-string`;
}

(async () => {
  let result: Result | undefined;

  await func().then(response => (result = response));

  if (!result) {
    return;
  }

    const { aa } = result.a;
  console.log(aa);

  (async () => {
    const { bb } = result.a; // TS18048: 'result' is possibly 'undefined'
  
    await func2(bb).then(response => console.log(response));
  })();
})();

In the above code, the first console.log works as expected ("aa"), but I cannot use the result variable inside the following function (func2) because of the following error: TS18048: 'result' is possibly 'undefined'.

What is the difference? Why was I able to use the deconstruct variable aa outside the last function, and while trying to deconstruct the result inside the last function, I'm getting this error?

tsconfig.json file:

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "noEmit": false,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "useDefineForClassFields": true,
    "sourceMap": true,
    "baseUrl": "./",
    "outDir": "./dist",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    },
    "typeRoots": [
      "./node_modules/@types",
      "./src/types"
    ],
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "webpack/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

package.json file:

{
  "name": "wrapper",
  "license": "UNLICENSED",
  "type": "module",
  "scripts": {
    "start": "webpack-dev-server --config ./webpack/webpack.config.dev.js",
    "build:stage": "webpack --config ./webpack/webpack.config.stage.js",
    "build:prod": "webpack --config ./webpack/webpack.config.prod.js"
  },
  "devDependencies": {
    "@babel/core": "7.20.12",
    "@babel/plugin-proposal-class-properties": "7.18.6",
    "@babel/plugin-proposal-object-rest-spread": "7.20.7",
    "@babel/preset-env": "7.20.2",
    "@babel/preset-typescript": "7.18.6",
    "@types/google-publisher-tag": "1.20220926.0",
    "@types/node": "18.11.18",
    "@typescript-eslint/eslint-plugin": "5.48.0",
    "@typescript-eslint/parser": "5.48.0",
    "babel-loader": "9.1.2",
    "eslint": "8.31.0",
    "eslint-config-prettier": "8.6.0",
    "eslint-plugin-prettier": "4.2.1",
    "eslint-webpack-plugin": "3.2.0",
    "html-webpack-plugin": "5.5.0",
    "prettier": "2.8.2",
    "ts-loader": "9.4.2",
    "ts-node": "10.9.1",
    "typescript": "4.9.4",
    "webpack": "5.75.0",
    "webpack-cli": "5.0.1",
    "webpack-dev-server": "4.11.1",
    "webpack-merge": "5.8.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead",
    "not ie <= 11"
  ]
}

CodePudding user response:

In general, when the compiler is performing nullability analysis in the body of a lambda, there's very little it can assume about the content of any captured variables.

Consider this slight variant of your method:

const func2 = async (val: string): Promise < string > => {
  // some logic
  return `${val}-string`;
}

(async () => {
  let result: Result | undefined;

  await func().then(response => (result = response));

  if (!result) {
    return;
  }

    const { aa } = result.a;
  console.log(aa);

  const f = (async () => {
    const { bb } = result.a; // TS18048: 'result' is possibly 'undefined'
  
    await func2(bb).then(response => console.log(response));
  });
  f();
})();

When compiling f, all it knows is that result is captured. It cannot use any control flow logic from before the definition of f to "know" that result is not undefined.

It doesn't attempt to reason about future code it's not compiling yet. It doesn't know (as we humans do) that a) f is invoked immediately and b) that f will not be stored anywhere such that it can be invoked later.

We as humans can see an easy way that result could become undefined between the lambda being compiled and it being invoked:

  const f = (async () => {
    const { bb } = result.a; // TS18048: 'result' is possibly 'undefined'
  
    await func2(bb).then(response => console.log(response));
  });
  result = undefined;
  f();

You could argue that immediately invoked lambdas that aren't captured in explicit variables could be a special case since we then know that they are invoked exactly once in the same control flow state as before the lambda is defined.

But that would still require looking ahead beyond the body of a lambda (to know that it's immediately invoked) and now you have two different "flavours" of nullability analysis under which lambda bodies might be analysed.

You know that result isn't undefined in your usage. Use the tools already at your disposal to inform the compiler of that fact that it isn't designed to deduce for itself.

  • Related