itaydafna
itaydafna

Reputation: 2086

Type not inferred when accessed as object key

Given the following example:

type Dictionary = {
  [key: string] : string | undefined
}

function greet(name: string) {
  return 'Hello ' + name + '!';
}

function callGreetError(key: string, d: Dictionary) {
  if (typeof d[key] !== 'undefined') {
    return greet(d[key]) // TS Error: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.Type 'undefined' is not assignable to type 'string'.(2345)
  }
  return key + ' is not in dictionary';
}

function callGreetNoError(key: string, d: Dictionary) {
  const storedVal = d[key];
  if (typeof storedVal !== 'undefined') {
    return greet(storedVal) // Error goes away when value is stored in an external var.
  }
  return key + ' is not in dictionary';
}

I'm trying to understand why on callGreetError the type of d[key] inside the if block isn't inferred to be a string even though I'm explicitly telling TS it isn't undefined.

And why storing the value of d[key] on an external var storedVal on callGreetNoError fixes this error.

Upvotes: 4

Views: 1688

Answers (4)

ford04
ford04

Reputation: 74500

Basically TS won't narrow the property access type of computed property names like key.

const d: Dictionary = {...};
const key = "foo";
const fooProp = d[key];

// using .length as string type check
d["foo"] !== undefined && d["foo"].length; // ✔
fooProp !== undefined && fooProp.length; // ✔
d[key] !== undefined && d[key].length; // error, possibly undefined

It is not, because TS does some mutability check and warns that d[key] value could have changed between the check and its usage. For example following code is perfectly fine for the compiler, but may throw at run-time:

const testDict: Dictionary = {
  get foo() { return Math.random() > 0.5 ? "hey" : undefined }
};

function callGreetError(d: Dictionary) {
  // compiles fine, but throws error at run-time from time to time
  if (d["foo"] !== undefined) d["foo"].length
}
callGreetError(testDict)

To allow proper variable narrowing with control flow, TS must clearly know, what property you mean: by property access with dot notation d.foo or with bracket notation and a literal like d["foo"].

The "trick" with const storedVal = d[key] works, because TS infers the variable type of storedVal to be string | undefined. As control flow analysis in general is based on variables, the compiler now has an easier time narrowing down storedVal with a check for undefined.

Playground

Upvotes: 3

Krzysztof Krzeszewski
Krzysztof Krzeszewski

Reputation: 6714

Just create a temporary variable so you are not accessing the value separately every time. For all typescript knows the value could have changed between the check and its use:

function callGreetError(key: string, d: Dictionary) {
  const temp = d[key];
  if (temp !== undefined) {
    return greet(temp)
  }
  return key + ' is not in dictionary';
}

In this case both checks typeof temp !== "undefined" and temp !== undefined would work

The simplest example showing the problem can be manually assigning a get operator to the dictionary key. In the code sample below you can see that the value of key would be indeed always a string, satisfying type requirements, however it changes with every access

const testDict: Dictionary = {};

Object.defineProperty(testDict, "key", {
  get() { return Math.random().toString() }
});

console.log(testDict.key);
console.log(testDict.key);

Thus checking the type with first access and using it with the second is unrelated

Upvotes: 1

Robert Andrei
Robert Andrei

Reputation: 297

Adding an ! to d[key] in the return statement tells TypeScript that d[key] will not be undefined in this case, although normally it can be. This is known as a non-null assertion operator.

function callGreetError(key: string, d: Dictionary) {
  if (typeof d[key] !== 'undefined') {
    return greet(d[key]!) 
  }
  return key + ' is not in dictionary';
}

A new ! post-fix expression operator may be used to assert that its operand is non-null and non-undefined in contexts where the type checker is unable to conclude that fact. Specifically, the operation x! produces a value of the type of x with null and undefined excluded. Similar to type assertions of the forms x and x as T, the ! non-null assertion operator is simply removed in the emitted JavaScript code.

Upvotes: 1

Sagi Rika
Sagi Rika

Reputation: 2979

Try doing it like this:

function callGreetError(key: string, d: Dictionary) {
  return d?.[key]
    ? greet(d[key])
    : key + ' is not in dictionary';
}

This way you're making sure that key is not undefined, and not only d[key]. If the syntax confuses you, you can read more about optional chaining here.

Upvotes: -1

Related Questions