Home > Back-end >  TypeScript ignoring type of const. Why does this compile?
TypeScript ignoring type of const. Why does this compile?

Time:07-20

In certain cases I set the type of a const, and TypeScript ignores it. In particular, sometimes I add | undefined and TypeScript ignores the fact that the value might be undefined. Am I missing something?

I'm grabbing an element from an Array or a NodeList. I'm saving it in a let or a const. By default TypeScript assumes that an array access will never fail. I know this particular array access is very likely to fail. I'm trying to mark the type of the expression so when I try to use it later I don't forget to check for undefined. Here's an example:

const myArray = ["a", "b", "see", "d", "e", "f"];

// Default:
export const plain0 = myArray[0];    // "a"
export const plain9 = myArray[9];    // undefined
export const plainMissing = undefined; // undefined

console.log(plain0.length);  // This will work as expected.
console.log(plain9.length);  // This will cause a runtime error.
console.log(plainMissing.length);  // Does not compile.

// With explicitly variable declarations:
export const variableType0 : string | undefined = myArray[0];
export const variableType9 : string | undefined = myArray[9];
export const variableTypeMissing : string | undefined = undefined; // undefined

console.log(variableType0.length);  // Compiles but shouldn't!!!!
console.log(variableType9.length);  // Compiles but shouldn't!!!!
console.log(variableTypeMissing.length);  // Does not compile!

VS Code and tsc agree on the results. They both ignore my type declarations and allow me to access a string | undefined as if it was a string. The compiler is ignoring my declarations. It's treating variableType* in my code just like plain*.

I asked tsc to generate a .d.ts file for me. That confirms that it knows the second set of constants should have different types than the first set.

export declare const plain0: string;
export declare const plain9: string;
export declare const plainMissing: undefined;
export declare const variableType0: string | undefined;
export declare const variableType9: string | undefined;
export declare const variableTypeMissing: string | undefined;

When I'm in VS Code and I hover my mouse over any of the const statements, the tool tips show me the same types as the .d.ts files. These are the values I'm expecting.

But when I hover my mouse over any of the log statements, I see different types. On those lines, the tool tips for variableType0, variableType9, and variableTypeMissing are string, string, and undefined, respectively. Those types perfectly match the errors that I see. But those types do not match their declarations.

Is there some reason that TypeScript is ignoring my declarations? Is there something I'm doing wrong? Am I misunderstanding the rules? Is it a bug in the compiler? How can I mark the code so the compiler will force me to use ?. not just .?

This is my tsconfig.json. It mostly comes from vite, but I changed a few things.

{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./types",
    "noImplicitAny": true,
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ESNext", "DOM"],
    "moduleResolution": "Node",
    "strict": true,
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "emitDeclarationOnly": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

EDIT / Simpler Example

const x : number|undefined = Math.random();
x.toFixed();

const y : number|string = Math.random();
y.toFixed();

This version does not use arrays. I'm running into the same problem. TypeScript thinks it knows the type of the variable. I'm telling TypeScript that I know better by offering a declaration. TypeScript is not giving me an error, saying it doesn't like my declaration. But then it ignores my declaration.

CodePudding user response:

Addressing your simplified example:

const x : number|undefined = Math.random();
x.toFixed();

TypeScript can sometimes narrow the apparent type of a variable/property/expression.

In particular, when a variable is of a union type (like number | undefined), the compiler will narrow the apparent type of that variable upon assignment to just those union members compatible with the value being assined. Since Math.random() is of type number, the assignment x = Math.random() will narrow the apparent type of x from number | undefined to number.

This is generally seen as desirable behavior; without narrowing you would never be able to type guard values; a check like if (x !== undefined) x.toFixed() would not work because the check x !== undefined would have no bearing on the apparent type of x. Without assignment narrowing, if (x !== undefined) x = 5; followed by x.toFixed() would be an error.

In any case, if you don't like this behavior, the only way to fix it is to make sure the value being assigned is widened to the full union type before the assignment. The easiest way to do this is to use a type assertion on this value. The expression Math.random() as number | undefined is of type number | undefined, so if you assign that to x you will get no narrowing:

const y = Math.random() as number | undefined;
y.toFixed(); // error

Playground link to code

CodePudding user response:

I found a solution.

export const expressionType0 = myArray[0] as string | undefined;
export const expressionType9 = myArray[9] as string | undefined;

console.log(expressionType0.length);  // Does not compile!
console.log(expressionType9.length);  // Does not compile!

This does what I want. It tells TypeScript to ignore its normal rules. Just in these two cases array indexes must be checked. VS Code even suggested that I replace the . with a ?..

I still wish I understood why my first attempt failed. Casting the expression worked exactly as I wanted. But casting the variable did not.

  • Related