Reputation: 2086
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
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
.
Upvotes: 3
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
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
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