-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Restrict template literal interpolation expressions to strings #30239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
So I actually think this is a context where coercion to string is clearly expected, and it makes template literals easier to read and work with. If you want the build to fail if name is null, then why in your example is name explicitly nullable? Perhaps the example could be clearer to show a compelling situation. If its just about output format, you could write something like |
This is a contrived example; obviously I wouldn't write a function like this. In real work, we deal with complex data structures that have many fields of differing types. We process data that is far removed from its type definitions. That's the whole point of static typing. I wouldn't write a function so narrowly purposed that allowed The point of type safety is to protect you from mistakes. If people always correctly grokked the type of every variable they used in practice, then we wouldn't need typescript at all. But people don't. I might have a structure:
If I did this:
I get "Joe undefined Blow lives at [Object object]" This would be a very easy mistake to make. Personally, there's never a time when I want type coercion. I don't see why this is a place you'd want it, any more than you'd want it when assigning anything else to a variable of type BTW, the use case that this came up had to do with refactoring. I had to take an existing type and change it from |
Regarding this:
I'd be fine with this - thought I'd probably say But I think it would also be reasonable to accept both But I would never want I thinks this should be optional, as a compiler directive, like many other options that enforce stricter typing requirements. |
I agree that implicit coercion of |
I also agree that there can be situations where you intentionally want to do that. But in practice, mistakes happen far more than those use cases. I'd much rather say Refactoring is really where it becomes a killer. When I went through the exercise today of having to manually inspect every bit of code that consumed the structure I'd refactored to see if it was involved in any templates, and also wondering if there were any derived usages I missed, it felt like a big hole. |
The core problem here is that people add custom |
The usage of good
You'd have to say:
I don't see why you'd want to treat string templates any differently as part of a comprehensive static typing system. This is the only place you're allowed to not explicitly call I don't really like "toString()" anyway as part of production code - while it's handy for console logging or debugging interactively, I wouldn't want to use it for real output. Overriding the prototype method provides engineers with no information about what's actually being output for the object. Anything that's not trivial almost certainly has more than one possible format in which it's output could appear, so I think it's much better to use expressive names for string formatting methods. Again - I'd much rather be required to explicitly invoke a method when using an object in a string template. In my perfect TypeScript world there's no "good" I certainly understand some people might feel differently, hence making it optional seems reasonable for this reason and backward compatibility. But at the same time I think the notion of being able to disable coercion for string templates has value and would benefit users who prefer better type safety over convenience. I can't think of any other exceptions to "no coercion" (other than using |
Here's something else interesting related to this. In designing classes, we'd actually like to forbid the use of string coercion. It seems like this should be possible:
But TypeScript has no problem with this. Probably I just don't understand the totality of how |
I ran into this with something like: console.log(`Name is ${name}`); Name was not defined in my scope, but in
So my code compiled but at runtime I got:
Result: firebase/firebase-tools#1241 |
We had a similiar issue. We have split shared code into libs, which are used in several services. Every lib is used in at least 2 services. Someone expanded an interface, replacing a In our code we used a string template like this |
on node:
on ts-node:
But If you use how to change this behavior on other target? |
I've also run into some frustration with this. Not only with template strings, but similarly with the One possible solution would be to add a way to disable implicit coercion on individual types defined in TS (as opposed to native ones)? Just a thought. |
Just hit this issue myself. It's very easy to start relying on the compiler to catch silly stuff, and miss something inane like forgetting to call a function; it even made it through review unnoticed. In my case, our CI acceptance tests were failing with a very peculiar looking output... turns out the template literal was coercing a function, per the above. If you've got the following: const fn = (): string => 'a string';
const x = `/blah/blah/${fn()}`; And you forget to call In terms of safety, flexibility, and being as helpful as possible to as many developers as possible, I think this warrants a fix. |
Is there any workaround for this now? |
I need this too |
typescript-eslint has a |
This is also a problem for us where we have a lot of methods and getters for building template literals. If the arguments are omitted we get runtime errors. Following is my naive attempt: const neverString = <T>(arg: T) => arg as T & { toString: never };
const f = neverString((x: number) => `${2 * x}`);
`${f(5)}`; // OK as expected
`${f.toString()}`; // Error as expected
`${String(f)}`; // OK - should be error?
`${f}`; // OK - should be error? I'm aware that there's a countervailing expectation that |
This seems to me like one of the weakest links in TypeScript's type system – in the weak–strong sense related to the presence and semantics of implicit type coercion (not static–dynamic, explicit–inferred, expressive–inexpressive or any other type-system-defining dimensions that are often confused with each other or used ambiguously). What often happens for me in practice is this:
export default {
url: `https://example.com`,
} as const;
import T from "./text";
console.log(`Your URL: ${T.url}`); Then I refactor/add a new feature/whatever such that export default {
url: (user: string) => "https://example.com/" + user,
} as const; Or perhaps this: export default {
url: {
external: "https://foo.bar",
profile: (user: string) => "https://example.com/" + user,
},
} as const;
$ ghci
> "" ++ id
<interactive>:1:7: error:
• Couldn't match expected type ‘[Char]’ with actual type ‘a0 -> a0’
• Probable cause: ‘id’ is applied to too few arguments
In the second argument of ‘(++)’, namely ‘id’
In the expression: "" ++ id
In an equation for ‘it’: it = "" ++ id
> "" ++ show id
<interactive>:2:7: error:
• No instance for (Show (a0 -> a0)) arising from a use of ‘show’
(maybe you haven't applied a function to enough arguments?)
• In the second argument of ‘(++)’, namely ‘show id’
In the expression: "" ++ show id
In an equation for ‘it’: it = "" ++ show id (GHC even points out the actual cause of the problem in these cases, namely that I haven't applied |
I would extend this suggestion to also include string concatenation with the That said in addition to the rule @ark120202 mentioned, this ESLint rule that specifically handles objects without a meaningful |
I'd like to add a much worse example of this: let a = ["one", "two"];
let s = `<ul>` + a.map(v => `<li>${v}</li>`) + `</ul>`; It's not immediately obvious that there's anything wrong with this code, and TS doesn't think so either. The rendered result, however, includes commas between the I was quite surprised that this is allowed in the first place? While arrays and objects are capable of coercion via I would propose adding a new compiler option to prevent implicit object to string conversions - and consider making it default for I think there's an inspection for this in ESLint? I don't use ESLint and haven't felt the need - since I've decided to use TS for type-hecking, it would be nice if I didn't need additional tooling for something that is essentially just a type-check. |
There is! In fact, there are two that can cover it:
Mood. |
Heck, I might even use it for type-checking. |
I just fixed an annoying bug that would have been caught by this. I was doing string interpolation to construct an URL in CSS. I can't imagine anyone ever intends to load |
If you want a workaround here using TypeScript, here's a function: function s (strings: Readonly<string[]>, ...values: Readonly<string[]>): string {
let str = ''
let i = 0
for (; i < values.length; i++) {
str += strings[i]
str += values[i]
}
return str + strings[i]
}
s`${undefined}` // => Argument of type 'undefined' is not assignable to parameter of type 'string'. I agree this behavior should be some sort of tunable |
I would not want to limit it to strings. for objects: |
since there was a shortage of good motivating examples at the beginning of this thread: async function get_f() {
return "f";
}
(async function main() {
const f = get_f(); // oops, forgot the `await`
const oo = "oo";
const foo = f + oo; // or `${f}${oo}`
console.log(foo); // [object Promise]oo
})(); It's obvious that it's a type error where someone just forgot to |
@beauxq How does that need a template?
That's what a linter is for. |
Did I suggest that anything needs a template? I don't understand this question.
It's a type error. That's what a static type analyzer is for. This is the first thing on the list of TypeScript design goals. |
It's perfectly valid and reasonable to use promises without async/await sugar. The problem is coercion. |
That's correct. In this example, it's obvious that it's a type error where someone just forgot to await the promise. And the coercion is the reason the type error isn't found. |
No, because the |
No one is "pinning the problem on the lack of |
When I created this issue I tried to show a common cases involving coercion, but out of context it's easy for them to be dismissed as contrived. I think the promise one is good, but maybe slightly confusing also because of the lack of context? There are really two separate issues, though, that I don't think I thought through when I initially made the issue and probably should be considered separately. One is coercion of non-string values such as The other is the automatic use of native There are good linting solutions to this problem now, so honestly I don't care as much as I did 3 years ago, though I do still think it's within the goals of Typecript to forbid this usage, and I also think it's more consistent with typechecking in other contexts. -- Type checking is enforced when assigning something to an existing These are two different issues from an actual javascript standpoint (since the former would be changing the type of the variable); but from a "how do statically typed languages work" standpoint it seems inconsistent. Examples:
|
I'd just like to mention that code examples become more readable with syntax highlighting. 🙂 It's easy to achieve in Markdown: ```ts
console.log("Hello, World!")
``` Result: console.log("Hello, World!") |
This is a common source of bugs for us - and Typescript seems like the right place to be preventing them. |
I found a temporary workaround with Typescript v4.0.3 using Tagged templates. export function s(strings: TemplateStringsArray, ...values: string[]): string {
return strings.reduce(
(result, string, index) => result + string + (values[index] ?? ""),
""
);
}
let myString = s`hello world ${123} yo ${"lo"}`;
console.log(myString) The above code will yield the following error:
This works because the type of The downside however is of course you have to import Nonetheless, that can be automated using the Typescript parser found in Typescript ES Tree. I'm not going to give every detail about the automation as it can be too lengthy. Let me know if you're interested. |
The issue was an object in a template string. It might be worth requiring template strings to pass in *only* strings (or numbers?) but this requires a bit of a workaround currently: microsoft/TypeScript#30239 (comment)
Why even bother with this rule? |
A string variable is interpolated. You refactor and it's now something else. You almost certainly want to change how it's being interpolated, but the compiler doesn't flag it. So, the same as why (among other reasons) you want static type safety everywhere else. |
What I understand is that I'm refactoring the variable, but when I use a template string, it should be forced to be converted to string now. Can you give me an example in response to what you said? |
This thread already includes many examples, have you read it from the beginning? One of the central purposes of TypeScript, at a very high level, is that we want to prevent "forcing" conversion of one type to another and allow engineers to require such conversions to be explicit. |
Please read it carefully if you want to understand this proposal.
It's really not possible to know why the lint rule isn't working as expected without further context including your eslint and tsconfig files, typescript version, and so on. This exact same example works fine for me trivially. More generally, though, this is not a support forum. You might try asking this question on stack overflow. |
You're right. Thank you. |
In template literal types, interpolations are required to be type A = `before${unknown}after` // error
type B = `before${string | number | bigint | boolean | null | undefined}after` // ok If a compiler option to require using only |
Some URLs here are broken. https://typescript-eslint.io/rules/restrict-template-expressions/ |
Search Terms
"template literal"
There are many hits, some which seem related, but everything I could find was a much bigger ask or wider in scope
Suggestion
Add a compiler option to enforce using only strings in ES6 string template literals
Use Cases
When using string literals, any variables are coerced to strings, which can lead to undesirable behavior.
As far as I can tell there's no way to avoid this behaviour. In the spirit of tying everything, I'd prefer that only actual
string
types are permissible for use in string templates to avoid accidental coercion by passing null, undefined or object types that may have unexpected string representations, and force users to explicitly convert them to strings.Examples
For example:
Ideally the compiler would fail since
name
can be null.Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: