microsoft/TypeScript

Try to preserve template-ness of string literal types where possible

AlCalzone opened this issue ยท 9 comments

Search Terms

template literal preserve

Suggestion

Now that we have template literal types, I would love to have the ability to apply them to string template literals.

Use Cases

This would be really useful in the ioBroker type declarations where we try to infer the object types returned by some methods from the given IDs. These IDs can be directly specified in the method calls, but are in reality often built from multiple parts.

A dumbed down example is shown in the following examples:

Examples

Playground

declare function takesLiteral<T extends string>(literal: T): T extends `foo.bar.${infer R}` ? R : unknown;

const t1 = takesLiteral("foo.bar.baz"); // "baz"
const id2 = "foo.bar.baz";
const t2 = takesLiteral(id2); // "baz"

declare const someString: string;
const t3 = takesLiteral(`foo.bar.${someString}`); // expected: string, actual: unknown

const id4 = `foo.bar.${someString}`;
const t4 = takesLiteral(id4); // expected: string, actual: unknown

Both id4 and the string passed to the third call of takesLiteral are string constants that are created with a template literal. TypeScript should be able to preserve that literal-ness, since it already knows we're feeding a string into a template string.

This means we could work with literals of type foo.bar.${string}, not string like we currently do.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

This is already covered by #40707 with the caveat that you need to include as const to get template literal types. It would be a breaking change to do it for all template literal expressions, so I think that's the best we can do.

@ahejlsberg cool, that is good to know. Can you elaborate how that would be breaking? These literals should still be assignable to string?

The mentioned PR only applies to TypeScript though, not JavaScript. Is there any way to get the desired behavior in JS?
I can't use as const there, nor can I explain all the users of my API (often beginners) how to use JSDoc type casts (is there even one for as const?) as a workaround.

Can you elaborate how that would be breaking?

We'd end up giving more specific types to variables with types inferred from initializers, which would be breaking. However, we could consider introducing widening and non-widening template literals akin to what we do for regular literal types (see #10676 and #11126 for context).

However, we could consider introducing widening and non-widening template literals akin to what we do for regular literal types

I don't quite grasp the implications of this change but this seems like it might cover most of the use cases I have in mind.

Now implemented in #41891.

We tried this and it turned out to be a bit too breaky and didn't really model user expectations. We can reconsider something that avoids the pain that the last one added, but it's unclear what that would be. In the meantime, you can use as const on your template strings.

Please limit discussion to useful additions, thoughts, or insights - no :+1:s or "just as a use case"s, since those won't be constructive. If you really need to do something like that, vote ๐Ÿ‘ on the issue itself.

I've raised #42884 to request support for the as const workaround in JS, because that is currently not an option for JS devs.

In #43376 (now merged) we fix this as much as possible without introducing breaking changes:

declare function takesLiteral<T extends string>(literal: T): T extends `foo.bar.${infer R}` ? R : unknown;

const t1 = takesLiteral("foo.bar.baz"); // "baz"
const id2 = "foo.bar.baz";
const t2 = takesLiteral(id2); // "baz"

declare const someString: string;
const t3 = takesLiteral(`foo.bar.${someString}`);  // string

const id4 = `foo.bar.${someString}` as const;
const t4 = takesLiteral(id4);  // string

Note the use of as const in the assignment to id4. An alternative would be to have a `foo.bar.${string}` type annotation, but we won't infer a template literal type without some indication it is desired.