I trying to type a REST API using fancy TypeScript template literals. I have a system that works for const
string types and const
Javascript template literal types, but not for Javascript template literals with unknown values.
Consider the following recursive path parser.
type PathVariable = string;
type ExtractPathVariable<T extends string> = T extends `:${string}`
? PathVariable
: T;
type PathParts<Path extends string> = Path extends `/${infer Rest}`
? PathParts<Rest>
: Path extends `${infer Start}/${infer Rest}`
? [ExtractPathVariable<Start>, ...PathParts<`${Rest}`>]
: Path extends `${infer Item}`
? [ExtractPathVariable<Item>]
: never;
It gives expected results for some cases
// A == ["short", "url", "path"]
type A = PathParts<"/short/url/path">;
const b = `/short/url/path` as const;
// B == ["short", "url", "path"]
type B = PathParts<typeof b>;
// C == ["short", "${y}", "path"]
type C = PathParts<"/short/${y}/path">;
const d = `/short/${45}/path` as const;
// D == ["short", "45", "path"]
type D = PathParts<typeof d>;
However, the case I'm most interested in (because that's how I call my API), it doesn't work.
let y: unknown;
const e = `/short/${y}/path`;
// E == never
type E = PathParts<typeof e>;
Is there a way to make PathParts<typeof e>
work? A result of E == ["short", string, "path"]
or E == ["short", unknown, "path"]
would be fine.
Issue microsoft/Typescript#43060 has been marked fixed by pull request microsoft/Typescript#43361, which should be released with TypeScript 4.3. At that point, your code above will work (as long as you use a const
assertion as I mention below):
let y: unknown;
const e = `/short/${y}/path` as const;
// const e: `/short/${string}/path`
type E = PathParts<typeof e>;
// type E = ["short", string, "path"]
You can see it in action here: Playground link to code
Sorry, but I think this is currently not possible as of TypeScript 4.2.
First, to even get close to this behavior you'd need to use a const
assertion to tell the compiler that you'd like e
to be inferred as a template literal type and not just string
. While there is a consistency argument that, like const foo = "abc"
is inferred as "abc"
, so const bar = `abc${x}`
should be inferred as type `abc${string}`
or the like, as requested in microsoft/TypeScript#41631, this change ended up breaking too much real world code. So you need the as const
:
const e = `/short/${y}/path` as const
// const e: `/short/${string}/path`
But that's about as far as we can go. You're trying to match a template literal type with multiple infer
locations against another template literal type with a "pattern" literal like `${number}`
(where pattern literals are implemented in microsoft/TypeScript#40598. But according to microsoft/TypeScript#43060 this is not possible (it mentions `${number}`
instead of `${string}
, but the same situation occurs for all pattern literals):
type Simple<T> = T extends `${infer F}/${infer L}` ? [F, L] : never
type Works = Simple<`foo/bar`> // ["foo", "bar"];
type Broken = Simple<`foo/${string}`> // never
type AlsoBroken = Simple<`${string}/bar`> // never
That issue hasn't been classified as a bug/limitation/suggestion yet, but it is related to microsoft/TypeScript#43243 which is considered a suggestion. For now there doesn't seem to be a mechanism in place to allow this sort of inference to work. I haven't been able to find any workaround that behaves reasonably, either.
If you care about seeing this happen you might want to go to either of those issues and give a 👍 and/or describe why your use case is compelling.