Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integer value comparison, minimum length array, useful? #282

Closed
alita-moore opened this issue Oct 10, 2021 · 4 comments
Closed

Integer value comparison, minimum length array, useful? #282

alita-moore opened this issue Oct 10, 2021 · 4 comments

Comments

@alita-moore
Copy link

The following is an exploration into the limitations of typescript's ability to perform math comparisons. The primary limitation I found was the recursion depth being limited to 45. I don't think this is particularly practical because of this limitiation, but what do you think? Is there anything useful in the following?

// These are particularly useful when going from string -> number as to make this useful
type OneHundredNumber= [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99];
type OneTOOneHundred = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"10"|"11"|"12"|"13"|"14"|"15"|"16"|"17"|"18"|"19"|"20"|"21"|"22"|"23"|"24"|"25"|"26"|"27"|"28"|"29"|"30"|"31"|"32"|"33"|"34"|"35"|"36"|"37"|"38"|"39"|"40"|"41"|"42"|"43"|"44"|"45"|"46"|"47"|"48"|"49"|"50"|"51"|"52"|"53"|"54"|"55"|"56"|"57"|"58"|"59"|"60"|"61"|"62"|"63"|"64"|"65"|"66"|"67"|"68"|"69"|"70"|"71"|"72"|"73"|"74"|"75"|"76"|"77"|"78"|"79"|"80"|"81"|"82"|"83"|"84"|"85"|"86"|"87"|"88"|"89"|"90"|"91"|"92"|"93"|"94"|"95"|"96"|"97"|"98"|"99";
type OneToOneHundredNumber = 0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99

// Having anything greater than 45 isn't practical because it exceed typescripts depth limit
type FortyFive = '0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|end'

type ListOfNumbers = FortyFive;

// Just some helpers
type Decompose<S> = S extends `${infer _}|${infer Num}` ? [_, Num]  : never;
type ToString<T> = T extends number ? `${T}` : never;
type ToNumber<T extends OneTOOneHundred> = OneHundredNumber[T]

// max for LessThanThis is 45, ToBeCompared can be any integer
type LessThan<ToBeCompared extends number | string, LessThanThis extends number | string, Current=ListOfNumbers> =
  ToString<ToBeCompared> extends Decompose<Current>[0]
    ? ToString<ToBeCompared> extends ToString<LessThanThis>
        ? false
        : true
    : ToString<LessThanThis> extends Decompose<Current>[0]
      ? ToString<LessThanThis> extends Decompose<Current>[0]
        ? false
        : true
      : Decompose<Current> extends never
        ? false
        : LessThan<ToBeCompared, LessThanThis, Decompose<Current>[1]>

// max for GreaterThanThis is 45, ToBeCompared can be any integer
type GreaterThan<ToBeCompared extends number | string, GreaterThanThis extends number | string> = LessThan<GreaterThanThis, ToBeCompared>

// is 1 greater than 2?
type Test1 = GreaterThan<1, 2> // -> false
// is 2 greater than 2?
type Test2 = GreaterThan<2, 2> // -> false
// is 3 greater than 2?
type Test3 = GreaterThan<3, 2> // -> true

// is 2 less than 1?
type Test4 = LessThan<2, 1> // -> false
// is 1 less than 1?
type Test5 = LessThan<1, 1> // -> false
// is 0 less than 1?
type Test6 = LessThan<0, 1> // -> true

///////////// Building off of this for a practical use case...
// @ts-expect-error ignore because typescript doesn't allow for minutia
type DecomposedNumber<S> = ToNumber<Decompose<S>[0]>
// max is 44 for LessThanThis and RangeToReturn can be any length 0 | 1 | 2 ... | infinity
type AllGreaterThan<LessThanThis extends number | string, RangeToReturn extends number, Current = ListOfNumbers> =
  ToString<LessThanThis> extends Decompose<Current>[0]
    ? Exclude<RangeToReturn, DecomposedNumber<Current>>
    : AllGreaterThan<LessThanThis, Exclude<RangeToReturn, DecomposedNumber<Current>>, Decompose<Current>[1]>

// max is 44 for first, secondary can be any size
type G = AllGreaterThan<44, OneToOneHundredNumber>

type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift';
export type MinLengthArray<Element, Length extends 0 | 1 | 2 | 3 | 4, ArrayPrototype = [Element, ...Element[]]> = Pick<
	ArrayPrototype,
	Exclude<keyof ArrayPrototype, ArrayLengthMutationKeys>
> & {
	[index: number]: Element;
	[Symbol.iterator]: () => IterableIterator<Element>;
	readonly length: Length | AllGreaterThan<Length, OneToOneHundredNumber>;
};

// Does not error
const example1: MinLengthArray<string, 3> = ["1", "2", "3", "4", "6"]
// Does not error
const example2: MinLengthArray<string, 3> = ["1", "2", "3"]
// Does error (2 is not in 3 | 4 | 5 ... 99, based on my input above)
const example3: MinLengthArray<string, 3> = ["1", "2"]
@alita-moore
Copy link
Author

There an easier way to do the min length array.

[string, ...string[]]

Which is much better. Notably though it can't be used in comparisons / only works if the type is being defined in the place it is. I think this is relatively new so maybe a bug

@sindresorhus
Copy link
Owner

It's a cool exploration of TS, but I don't think it's very practical. You should definitely write a blog post about it. Happy to link to it from here if you do.

We could also maybe add [string, ...string[]] as a tip to the readme.

@sindresorhus
Copy link
Owner

What I think we could add is type NonEmptyArray<T> = [T, ...T[]];. I would use that sometimes. Maybe also for other types like object, set, and map.

@alita-moore
Copy link
Author

the problem I think with the NonEmptyArray in its invocation that you describe (although it also applies to one I did) is that it doesn't transfer over well to inference. For example

const foo = (bar: [string, ...string[]]) => {}

foo(["bar"]) // no error
const bar = ["bar"];
foo(bar) // Argument of type 'string[]' is not assignable to parameter of type '[string, ...string[]]'

Typescript's arbitrary type declarations and then literal comparison is the bane of my existence, lol. I spend way too much time trying to figure out ways around it. Any ideas on a work around? I'll create a ticket in typescript in the meantime though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants