Jim
Jim

Reputation: 385

Why TypeScript doesn't infer the correct type when redeclaring a variable

I was writing a code that goes a little bit like this:

function helloWorld(customName: string) {
  return `Hello, ${customName}`;
}

const myName: string | null = null;
const isValidName = myName !== null;

if (isValidName) {
  console.log(helloWorld(myName));
}

If you run this code in the TypeScript playground, you'll see TypeScript will complain that Argument of type 'null' is not assignable to parameter of type 'string'.

How's that the case, though? That code only runs when isValidName is truthy and isValidName can only be true if myName is not null. Hence, myName is a string here. Why this doesn't work?

Upvotes: 2

Views: 468

Answers (4)

Evert
Evert

Reputation: 99533

The other answers do a good job explaining workarounds, but don't explain why.

The simple fact is that Typescript is not yet sophisticated enough to understand multiple dependent variables.

It's very possible that in the future features for this will get added. There's no reason why this couldn't work, but I imagine it is simply difficult.

I hope one day this does work, because it also makes some of the stuff I'm doing simpler.

Upvotes: 0

Shivam Singla
Shivam Singla

Reputation: 2201

If you want to store the result of narrow downing in isValidName for further use and don't want to re-compute it, you can create a utility function like this

/**
 * @param a The precomputed result
 * @param _b The variable to be narrowed down
 * @template T The type to which `_b` will be narrowed down to when `a` is true
 */
function typeCheck<T>(a: boolean, _b: any): _b is T {
  return a
}

Use like this

if (typeCheck<string>(isValidName, myName)) {
  console.log(helloWorld(myName)); // string here
} else {
  console.log(myName) // null here
}

Playground

This can be helpful when computing long expressions like

if (
    isLoneAvailableCapacity(feed) &&
    (isAvailableCapacitySubbeamPath(link) || isAvailableCapacityConnectivityLegPath(link)) &&
    toAvailableCapacityKey(feed.availableCapacity) === link.availableCapacityKey
) {
    // do something
}

// snip....
// some more code

if (
    isLoneAvailableCapacity(feed) &&
    (isAvailableCapacitySubbeamPath(link) || isAvailableCapacityConnectivityLegPath(link)) &&
    toAvailableCapacityKey(feed.availableCapacity) === link.availableCapacityKey
) {
    // do something else
}

It can be replaced easily by

const isValidFeed = isLoneAvailableCapacity(feed) &&
    (isAvailableCapacitySubbeamPath(link) || isAvailableCapacityConnectivityLegPath(link)) &&
    toAvailableCapacityKey(feed.availableCapacity) === link.availableCapacityKey

if(typeCheck<Feed>(isValidFeed, feed)) {
  // do something
}

// snip....
// some more code

if(typeCheck<Feed>(isValidFeed, feed)) {
  // do something else
}

Upvotes: 0

Linda Paiste
Linda Paiste

Reputation: 42218

The way that you have written this, typescript sees that isValidName is a boolean constant and that's it.

You, the code author, know that the value of isValidName indicates something about the value of myName, but typescript doesn't consider the relationship between the two variables.

What you want is for your if statement to use a type guard. Essentially a type guard is a boolean value computed at runtime which depends on the value of myName and whether than boolean is true or false provides some information which narrows the type definition of myName.

As @falinsky pointed out, myName !== null is fine as a type guard if you use it inside the condition of your if statement. It says, "if this is true then myName is not null".

You can also create isValidName as a function which checks a name and determines that it is a string.

const isValidName = (name: string | null): name is string => {
  return name !== null;
}

if (isValidName(myName)) {
  console.log(helloWorld(myName));
}

Upvotes: 0

falinsky
falinsky

Reputation: 7428

As far as I understand - that fact that you're storing the result of myName !== null into the variable isValidName enforces the TypeScript to check that value in the runtime and behave appropriately (so calling helloWorld(myName) seems illegal because isValidName can be potentially either true or false in runtime).

However, if you change the check to be

if (myName !== null) {
  console.log(helloWorld(myName));
}

TypeScript will be able to detect that the only possible type of myName can be a string in those circumstances.

Upvotes: 2

Related Questions