Reputation: 378
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
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