himnabil
himnabil

Reputation: 378

How to define a recursively string literal type in Typescript

I want to define a type for a series of numbers separated by ':' in a string Examples : '39:4893:30423' , '232' , '32:39'

so here is what I tried :

type M = `${number}` | '' 
type ML = `${M}` | `${M}:${ML}` 
// ERROR : Type alias 'ML' circularly references itself.

why can't i do this ? do you have a workable alternative ?

Upvotes: 3

Views: 792

Answers (1)

Moa
Moa

Reputation: 1576

I don't know how TypeScript works deeply enough to explain why the compiler doesn't allow circular references in this particular use case, but we can achieve something similar with recursive concatenation of string literal types and some type inference.

From Typescript docs:

When used with concrete literal types, a template literal produces a new string literal type by concatenating the contents.

My guess is that, from the compiler POV, template literals might actually already use circular references themselves, therefore deeming them unreachable in user-land.

A naive implementation of this type could be to simply hard-code a type concatenation for every possible length needed. That works if you have predefined, small sets of values.

type Primitive = string | number | bigint | boolean | null | undefined

type M1 = number
type Separator = ':'
type Concat<A extends Primitive, B extends Primitive = never> = B extends never
  ? `${A}`
  : `${A}${B}`

type One = Concat<M1>
type Two = Concat<M1, M1>
type Three = Concat<TwoDigits, M1>

type ML =
  | M1
  | One
  | Two
  | Three
  | Concat<Concat<Two, Separator>, TwoDigits>

The above generates the following union type (which could be simply defined as such):

type ML =
  | number
  | `${number}${number}`
  | `${number}${number}${number}`
  | `${number}${number}:${number}${number}`

To allow for an arbitrary number of string literals we can use inference, array type destructuring and recursion:

export type Concat<T extends string[]> = T extends [infer F, ...infer R]
  ? F extends string
    ? R extends string[]
      ? `${F}${Concat<R>}`
      : never
    : never
  : ''

Here's a join function and ConcatS type with separators borrowed from this article.

export type Prepend<T extends string, S extends string> = T extends ''
  ? T
  : `${S}${T}`

export type ConcatS<T extends string[], S extends string> = T extends [
  infer F extends string,
  ...infer R extends string[],
]
  ? `${F}${Prepend<ConcatS<R, S>, S>}`
  : ''

function joinWith<S extends string>(separator: S) {
  return function <T extends string[]>(...strings: T): ConcatS<T, S> {
    return strings.join(separator) as ConcatS<T, S>
  }
}

// usage 
const result = joinWith(':')('13', '230', '71238')
// const result: "13:230:71238" = "13:230:71238"

If you're looking for an inverse derivation, you could coerce the number parts to a string[] tuple to cast each string to a string literal, and concat'em:

const tuple = <T extends string[]>(...args: T) => args

const m1 = tuple('39', '4893', '30423')
const m2 = tuple('232')
const m3 = tuple('32', '39')

type ML = ConcatS<typeof m1, ':'> | ConcatS<typeof m2, ':'> | ConcatS<typeof m3, ':'>

The above generates the following union type (which could be simply defined as such):

type ML = "39:4893:30423" | "232" | "32:39"

Upvotes: 3

Related Questions