Skip to content

Instantly share code, notes, and snippets.

@Kirens
Last active December 1, 2023 01:15
Show Gist options
  • Save Kirens/ffe64d8f730512100d709d89f6828f0b to your computer and use it in GitHub Desktop.
Save Kirens/ffe64d8f730512100d709d89f6828f0b to your computer and use it in GitHub Desktop.
Confusing

Confusing Recursive Conditional Types

Seems to me some kind of black magic goes on behind the scenes. The example below might not be an MRE, but it's prety intuitive. Experiment with it in the playground.

This was supposed to be a compile-time enforced ipv4 string. Using recursive conditionals we can ensure the desired union type without reaching the limit (> 10K). But it seems that counting recursions doesn't work unless each case is unique.

// Count<n> = [0, 1, ..., n]
type Count<Len extends number, Arr extends number[] = []> =
Arr['length'] extends Len ? Arr : Count<Len, [...Arr, Arr['length']]>
// Enumerate<n> = 0 | 1 | ... | n
type Enumerate<Len extends number, Arr extends number[] = []> =
Arr['length'] extends Len ? Arr[number] : Enumerate<Len, [...Arr, Arr['length']]>
// Tail<[a, b, c, ...]> = [b, c, ...]
type Tail<T extends any[]> = T extends [ any, ...infer End ] ? End : [];
// Pred<n> = n-1
type Pred<A extends number> = Tail<[...Count<A>]>['length']
// ByteNumber = 0 | 1 | ... | 255
type ByteNumber = Enumerate<256>
// IPv4String = `${ByteNumber}(.${ByteNumber})*`
type IPv4String<T extends string, V = T> =
T extends `${ByteNumber}.${infer R}`
? IPv4String<R, V>
: T extends `${ByteNumber}`
? V
: never
const testIPv4String = <T extends string>(n: IPv4String<T>) => n;
testIPv4String('105')
// testIPv4String('142.256') // never
testIPv4String('234.7.183.32.51.3.153')
// testIPv4String('0.0.0.0.00') // never
// IPv4Str = `${ByteNumber}.${ByteNumber}.${ByteNumber}.${ByteNumber}`
type IPv4Str<T extends string, i extends number = 3, V = T> =
i extends 0
? (T extends `${ByteNumber}` ? V : never)
: i extends 1
// Generic subtraction works, but we have one case for each number
? (T extends `${ByteNumber}.${infer R}` ? IPv4Str<R, Pred<i>, V> : never)
: i extends 2 // Try with `i extends 2 | 3` and notice how it fails
? (T extends `${ByteNumber}.${infer R}` ? IPv4Str<R, Pred<i>, V> : never)
: i extends 3
? (T extends `${ByteNumber}.${infer R}` ? IPv4Str<R, Pred<i>, V> : never)
: never
const testIPv4Str = <T extends string>(n: IPv4Str<T>) => n;
// testIPv4Str("65") // never
testIPv4Str("110.15.1.189")
// testIPv4Str("0.0.0.0") // never
testIPv4Str("1.2.3.4.5")
// IPv4 = `${ByteNumber}.${ByteNumber}.${ByteNumber}.${ByteNumber}`
type IPv4<T extends string, i extends number = 3, V = T> =
i extends 0
? (T extends `${ByteNumber}` ? V : never)
// IDK, is there some kind of caching or other optimization here?
: T extends `${ByteNumber}.${infer R}`
? IPv4<R, Pred<i>, V>
: never
const testIPv4 = <T extends string>(n: IPv4<T>) => n;
// All fail
testIPv4("65")
testIPv4("110.15.1.189") // Should work :(
testIPv4("0.0.0.0")
testIPv4("1.2.3.4.5")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment