I can build number like that:
type Digits = '0'| '1'| '2'| '3'| '4'| '5'| '6'| '7'| '8'| '9'
type NonZero = Exclude <Digits, '0'>
type Negative = '-'
type PositiveNumbers =
| NonZero
| `${NonZero}${Digits}`
| `${NonZero}${Digits}${Digits}`
| `${NonZero}${Digits}${Digits}${Digits}`
| `${NonZero}${Digits}${Digits}${Digits}${Digits}`
| `${NonZero}${Digits}${Digits}${Digits}${Digits}${Digits}` // Error: Expression produces a union type that is too complex to represent.(2590)
type Numbers =
| PositiveNumbers
| `${Negative}${PositiveNumbers}`
But then i'm limited to numbers from -99999 and 99999, and beside that using the type Numbers in other types make the compiler struggle. So i'm using a type to represent numbers as a constraint like so:
type ConstrainNumber <N extends string> =
N extends '0'
? unknown
: N extends `${infer Char}${infer Rest}`
? Char extends '0'
? never
: Char extends Negative
? ConstrainNumber <Rest>
: ConstrainNumberRec <N, Exclude <Digits, '0'>>
: never
type ConstrainNumberRec <S extends string, D = Digits> =
S extends `${infer Char}${infer Rest}`
? Char extends D ? ConstrainNumberRec <Rest> : never
: unknown
When using it on a type it works as expected:
type UseNumbers <N extends string & _N, _N = ConstrainNumber <N>> = N
type testValid = UseNumbers <'2875467'>
type testInvalid = UseNumbers <'02875467'> // Expected error: Type 'string' does not satisfy the constraint 'never'.(2344)
But when i'm using a type that uses UseNumbers it breaks
type UseUseNumbers <N extends string & _N, _N = ConstrainNumber <N>> =
UseNumbers <N> // Unwanted Error: type 'N' does not satisfy the constraint 'string & ConstrainNumber<N>'
How can i pass a constraint string to a type using as parameters a parameter using that same constraint ?
And further more, i'm not sure to guess why it breaks ? On UseNumbers <N> N is already constraint or no ?
CodePudding user response:
The main issue is that generic parameter defaults are not generic constraints:
type Foo<T extends number> = T;
type BadFoo = Foo<string>; // error, not satisfying constraint
type Bar<T = number> = T;
type DefaultBar = Bar; // number
type StillGoodBar = Bar<string>; // string
In the above, the type parameter T in Foo is constrained to number, while in Bar it is unconstrained but just defaults to number. So you can't write Foo<string>. If you just write Bar it will be Bar<number>, but nothing stops someone from writing Bar<string>. It's not an error, and the compiler cannot assume inside Bar that T will be a subtype of number.
In this definition,
type UseUseNumbers<N extends string & _N, _N = ConstrainNumber<N>> = ...
The _N type parameter is unconstrained. It can be anything at all, even though it defaults to ConstrainNumber<N>. Only N is constrained to string & _N.
Your primary use case here is that nobody specifies _N, and that UseUseNumbers<N> is equivalent to UseUseNumbers<N, ConstrainNumber<N>>, which compiles if and only if N extends string & ConstrainNumber<N>. That's a neat way to avoid the circularity constraint error that would happen if you wrote type Oopsie<N extends string & ConstrainNumber<N>>, but that trick only works by leaving open the possibility that _N does not depend on N.
And so the compiler cannot know that _N is anything in particular. Some rogue developer might write UseUseNumbers<string, unknown> or UseUseNumbers<"abc","abc" | "def"> or some other crazy off-label use of UseUseNumbers that happens to compile. This is probably unavoidable (although the avoidability or lack thereof is off-topic for the question) but also unlikely to actually happen especially if you document that _N is for "internal use only" (but again, off-topic). How much you want to account for this possibility is up to you.
Anyway, the compiler is upset with
type UseUseNumbers<N extends string & _N, _N = ConstrainNumber<N>> =
UseNumbers<N> // <-- error
Why? because UseNumbers<N> evaluates to UseNumbers<N, ConstrainNumber<N>> which (similarly to UseUseNumbers) only compiles if N extends string & ConstrainNumber<N>. But the compiler doesn't know what about the current N, which is only known to extend string & _N, and _N is unconstrained. So it doesn't compile.
The way to fix this is to just explicitly take the assumption that nobody's going to mess with _N in UseUseNumbers, and pass this down into UseNumbers:
type UseUseNumbers<N extends string & _N, _N = ConstrainNumber<N>> =
UseNumbers<N, _N> // <-- this fixes it
Since the _N in UseNumbers is unconstrained, you can write UseNumbers<N, _N>, which will compile if and only if N extends string & _N. But we know that's true by definition of N in UseUseNumbers. As long as people write UseUseNumbers<N> and do not pass a second type argument, then this will evaluate to UseNumbers<N> as desired.
