Type narrowing is not working in for loop.
How to fix to work type narrowing correctly in for loop?
Below is a simple example(Please run it on TS playground)
// string or null is unsure until runtime
const htmlElements = [
{ textContent: Math.random() < 0.5 ? "test": null },
{ textContent: Math.random() < 0.5 ? "test": null },
{ textContent: Math.random() < 0.5 ? "test": null },
];
const contents: {
content: string;
}[] = [];
// type narrowing work
if (typeof htmlElements[0].textContent === "string") {
contents.push({ content: htmlElements[0].textContent });
}
// type narrowing not work
for (const i in htmlElements) {
if (htmlElements[i].textContent && typeof htmlElements[i].textContent === "string") {
contents.push({
content: htmlElements[i].textContent // <-- Error: Type 'null' is not assignable to type 'string'. textContent: string | null
});
}
}
The type of i
in for (const i in htmlElements) {}
is string
(you might have expected number
, but enumerating keys of objects, even arrays, will give you strings and not numbers). And so htmlElements[i]
is obtained by indexing into an array with an unknown index. The compiler doesn't check this by checking the identity of i
; it does so by checking the type of i
. So the obvious truism htmlElements[i] === htmlElements[i]
is not seen by the compiler; if you had a j
of the same type as i
, you wouldn't be able to conclude that htmlElements[i] === htmlElements[j]
. And neither can the compiler. This inability for the compiler to track indexes unless they are of a single literal type is a longstanding issue in TypeScript, as described in microsoft/TypeScript#10530.
Until and unless this is addressed, you'd need to work around it. The standard workaround is to copy the value to its own variable and then do your checks on the variable, so you're only indexing once:
for (const i in htmlElements) {
const el = htmlElements[i];
if (el.textContent && typeof el.textContent === "string") {
contents.push({
content: el.textContent
});
}
}
Here el
is copied from htmlElements[i]
, and after this, all the narrowing works as expected.
However in this particular case you should refactor further. It is considered bad practice to iterate over the keys of arrays with a for...in
loop. Unless you actually need to look at the numeric index, in modern JavaScript, you should probably use a for...of
loop instead, which gives you each element as a variable to start with:
for (const el of htmlElements) {
if (el.textContent && typeof el.textContent === "string") {
contents.push({
content: el.textContent
});
}
}