StartsWith
StartsWith
Challenge
Implement StartsWith<T, U> which takes two exact string types and returns whether T begins with U.
type a = StartsWith<'abc', 'ac'> // expected: false
type b = StartsWith<'abc', 'ab'> // expected: true
type c = StartsWith<'abc', 'abcd'> // expected: falseSolution
type StartsWith<T extends string, U extends string> =
T extends `${U}${string}` ? true : falseTemplate literal pattern matching is all you need. If T can be expressed as U followed by anything, then T starts with U.
Breaking It Down
Template literal extends check
T extends `${U}${string}`${U}anchors the beginning: the string must start with whateverUis${string}matches zero or more remaining characters (the rest of the string)- TypeScript performs structural pattern matching — if
Tcan be decomposed this way, the condition istrue
Edge cases
T | U | Match? | Why |
|---|---|---|---|
'abc' | 'ab' | ✅ | 'abc' = 'ab' + 'c' |
'abc' | 'ac' | ❌ | 'ac' is not a prefix of 'abc' |
'abc' | 'abcd' | ❌ | U longer than T, can't match |
'abc' | 'abc' | ✅ | Exact match; ${string} matches '' |
'abc' | '' | ✅ | Empty string is a prefix of everything |
'' | '' | ✅ | Both empty |
'' | 'a' | ❌ | Non-empty U can't start empty T |
Deep Dive
Alternative: infer approach
type StartsWith<T extends string, U extends string> =
T extends `${U}${infer _Rest}` ? true : falseFunctionally identical — we just explicitly name the rest of the string (and ignore it with _Rest). The ${string} version is more concise since we don't need the captured tail.
Companion: EndsWith
The mirror challenge 2693-EndsWith is symmetric:
type EndsWith<T extends string, U extends string> =
T extends `${string}${U}` ? true : falseSwap the sides: ${string} anchors the beginning, ${U} anchors the end.
Runtime equivalent
This type mirrors the JS String.prototype.startsWith() method — just lifted entirely into the type system. No runtime code, no loops, no recursion: a single template literal pattern does all the work.
String template literal matching semantics
TypeScript resolves template literal extends checks by attempting to unify the pattern against the string literal type at compile time. This is sound and complete for finite literal types — it will always give the correct true/false result.
For generic string types (not literal), the result is itself generic:
type T1 = StartsWith<string, 'ab'> // boolean (not narrowed)
type T2 = StartsWith<'abc', string> // boolean (U is generic)When either side is a broad string, TypeScript can't determine true/false statically, so it widens to boolean.
Comparison with character-by-character recursion
An earlier, pre-template-literal approach would recurse over characters:
type StartsWith<T extends string, U extends string> =
T extends ''
? U extends '' ? true : false
: U extends ''
? true
: T extends `${infer TC}${infer TR}`
? U extends `${infer UC}${infer UR}`
? TC extends UC ? StartsWith<TR, UR> : false
: true
: falseThis is O(n) recursive, hits recursion limits on long strings, and is far harder to read. The template literal single-liner is strictly superior.
Key Takeaways
T extends \{string}`` is the idiomatic one-liner for prefix checking in the type system${string}matches any string suffix (including empty), effectively acting as a wildcard tail anchor- The same pattern can be flipped for
EndsWithby swapping${string}and${U} - Generic string types widen the result to
boolean— literal types are needed for a definitetrue/false
