Yuzhe's Blog

yuzhes

StartsWith

StartsWith

Challenge Link

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: false

Solution

type StartsWith<T extends string, U extends string> =
  T extends `${U}${string}` ? true : false

Template 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}`

Edge cases

TUMatch?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 : false

Functionally 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 : false

Swap 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
        : false

This 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