My understanding of TypeScript is not deep enough to provide a detailed explanation as to why circular references are not allowed in this specific scenario. However, a workaround can be achieved through recursive concatenation of string literal types and leveraging type inference.
As stated in the Typescript documentation:
When used with concrete literal types, a template literal creates a new string literal type by combining their contents.
I speculate that from the perspective of the compiler, template literals may already involve circular references internally, hence making them unattainable for users.
An elementary approach to implementing this type would be to manually define type concatenation for various required lengths. This method is suitable for situations with predefined and limited value sets.
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 results in the following union type (which could also be directly defined):
type ML =
| number
| `${number}${number}`
| `${number}${number}${number}`
| `${number}${number}:${number}${number}`
To accommodate an unlimited number of string literals, we utilize 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
: ''
Additionally, we introduce a join
function and ConcatS
type with separators borrowed from this referenced 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 wish to reverse engineer this process, you can convert the numeric parts into a string[]
tuple, cast each element to a string literal, and concatenate them:
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, ':'>
This will yield the following union type (which could alternatively be explicitly declared as follows):
type ML = "39:4893:30423" | "232" | "32:39"