Your EQ type looks innocent but is really insidious
You yourself used your type equivalence testing type EQ just within your LT version for comparing numbers. That's fine. However, I developed the idea to test my more complex types using your EQ type in the following way:
I define
type ComplextParameterizedType<P1, P2, ...> = ...
and construe test examples where I expect that ComplextParameterizedType<V1, V2, ...> evaluates to some SimpleType, where V1, V2 are instantiations of the respective type parameters. Accordingly I write tests such as
const test: EQ<ComplextParameterizedType<V1, V2, ...>, SimpleType> = true;
which I consider to have passed if the compiler does not complain. To be on the safe side, I can exchange true for false and observe whether the compiler complains as expected.
So I did develop the following type based on your arithmetic types:
type NumberRange<From extends number, To extends number, Result extends number = never> =
LT<From, Add<To, 1>> extends true
? (To extends Result ? Result
: NumberRange<Add<From, 1>, To, Result | From>)
: never;
It should give me a more convenient notation, say, NumberRange<3, 10> for 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10. So I tested:
const testRange1: EQ<NumberRange<3, 6>, 3 | 4 | 5 | 6> = true;
const testRange2: EQ<NumberRange<0, 0>, 0> = true;
const testRange3: EQ<NumberRange<3, 3>, 3> = true;
const testRange4: EQ<NumberRange<3, 6>, 3 | 4 | 5 | 6 | 7> = false;
const testRange5: EQ<NumberRange<3, 6>, 3 | 4 | 5> = false;
Everything seems fine, no compiler complaints. However, the cross-check for the first test, e.g., fails. That is, the compiler does not complain about
const testRange1: EQ<NumberRange<3, 6>, 3 | 4 | 5 | 6> = false;
either. In fact, the type of EQ<NumberRange<3, 6>, 3 | 4 | 5 | 6> evaluates to boolean, not as expected to true. This was extremely surprising and I spent some effort to figure out what is wrong with the extremely straigthforward and innocent looking EQ: It turned out that the mechanism called 'Distributive conditional types' (https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html) is the culprit.
Let's consider a simplified "half" version of EQ:
type IsMoreSpecific<Specific, General> = Specific extends General ? true : false;
And in fact, the compiler does not complain about
const test: IsMoreSpecific<3 | 4, 3> = true;
IsMoreSpecific<3 | 4, 3> evaluates to boolean rather than to false, as one might expect. According to the distribution mechanism, Specific extends General ? true : false with instantiation 3 | 4 for Specific and 3 for General evaluates to
(3 extends 3 ? true : false) | (4 extends 3 ? true : false) <==> true | false <==> boolean
Thus, because of these distributive conditional types, the innocent looking EQ is, in general, not appropriate for checking arbitrary types for equivalence (albeit it was harmless in your case as your only compared numbers). How to repair? Is there a trick to suspend the (in this case) unwanted distribution? Yes, there is, I found a simple one as the answer of jack-williams in https://github.com/microsoft/TypeScript/issues/29368: Simply wrap the type parameters in EQ as singleton arrays:
type EQ<A, B> =
[A] extends [B]
? ([B] extends [A] ? true : false)
: false;
In the course of this little adventure I remarked another potential abuse of your types you are not protected against yet: You do not really enforce, not even in your "hardened" Safe... variants of your types that only natural numbers pass where intended: Unions of numbers unfortunately pass the test as well, i.e.
IsPositive<3 | 4> evaluates to true and not to false (or ideally to never) as you might have hoped. This might not be a problem for the direct usage of your types but might in fact be problematic if they were used as building blocks for more complicated types.
The core problem seems to be that there is no way to distinguish unions of numbers from single numbers, both pass as sub-types of number. I did a little research but I found no way how to dissect union types into "their components" (such components are, of course, not even unique in the general case). Do you have an idea? I would be curious.
Thanks again for your nice arithmetic types. With the help of them I can now write the convenience tpyes like NumberRange which are already on my wishlist for a while.