Reputation: 67300
I'd like to pass variables into a template literal type value, but I can't seem to get it to work (you can play with the following example here). Is this possible?
type Bar = "a" | "b"
interface Foo {label: `${Bar} (${number})`}
const x: Foo = {label: "a (3)"}; // fine
const myBar: Bar = "b";
const myNum: number = 3;
const y: Foo = {label: `${myBar} (${myNum})`}; // errors
Is there any way of getting y
to compile?
Upvotes: 1
Views: 1511
Reputation: 327624
TypeScript 4.1 introduced template literal types as well as the ability to infer such types for template literal expressions... but only, as you noticed, via a const
assertion:
const y: Foo = { label: `${myBar} (${myNum})` } as const; // okay
This was implemented microsoft/TypeScript#40707, which was merged before the release of TS4.1.
It does seem reasonable to want template literal expressions assigned to const
variables to automatically be inferred as template literal types without needing an explicit as const
anywhere. After all, that's how string and numeric literals behave already:
let bStr = "b"; // uses let, so bStr is widened:
// let bStr: string
const bLit = "b"; // uses const, so bStr is a string literal, no "as const" required:
// const bLit: "b"
In fact, it seems so reasonable that, after TS4.1 was released, this exact change was implemented in microsoft/TypeScript#41891 and merged, as you can see if you use TS4.2-beta:
const y: Foo = {label: `${myBar} (${myNum})`}; // okay
UNFORTUNATELY, #41891 was a breaking change; real-world code using perfectly serviceable string
-typed template literal expressions would suddenly start seeing strange types and possibly even errors.
In the TypeScript design meeting on Jan 21, 2021 (see microsoft/TypeScript#42589) this breakage and weirdness was discussed. Apparently it was decided that automatically inferring template literal types would go too far. After all, not every const
variable is inferred to be as narrow as possible, and if you want narrower inference you can use as const
:
const arr = [1, 2, 3];
// const arr: number[]
const constArr = [1, 2, 3] as const;
// const constArr: readonly [1, 2, 3]
const obj = { a: "hello", b: "goodbye" };
// const obj: { a: string; b: string; }
const constObj = { a: "hello", b: "goodbye" } as const;
// const constObj: { readonly a: "hello"; readonly b: "goodbye"; }
So it was reverted in microsoft/TypeScript#42588 and merged on or around Feb 3, 2021. And thus your code that yields an error in TS4.1 will still be in error in TS4.2 (unless yet another reversal happens I guess):
Playground link, TS4.2-dev.20210207
Oh well. If you want template literal expressions to be inferred as template literal types, you will just need to use a const
assertion. Maybe in the future this will change, but for now I suppose we must automatic template literal inference as a failed/aborted experiment. What a journey!
Upvotes: 1
Reputation: 15096
As you already discovered, you can type it by using as const
:
const y: Foo = {label: `${myBar} (${myNum})` as const}
The reason is that by using as const
, TypeScript will narrow the type from string
to the the actual template literal type. If you look at the label without as const
, the inferred type will be string
, which is not assignable to ${Bar} (${number})
(surrounded by backticks, which are a pain in inline markdown code):
const l1 = `${myBar} (${myNum})` // inferred type: string
If you use as const
, the type gets narrowed to b (${number})
(with backticks), which is assignable:
const l2 = `${myBar} (${myNum})` as const // inferred type: `b (${number})`
The ${number}
in the type appears because of the explicit number
signature in const myNum: number = 3
. If you use const myNum = 3
, the inferred type becomes b (3)
(surrounded by backticks).
Upvotes: 1
Reputation: 67300
This gets it to compile, but I don't understand why I need to do it:
const z: Foo = {label: `${myBar} (${myNum})` as const}; // fine
I'll accept this as the answer if nobody else wants to chime in and explain.
Upvotes: 0