Wie implementiert man eine Typ-Ebene Max-Funktion über eine Vereinigung von Literalen in TypeScript?
Gegeben eine Vereinigung von Zahlenliteralen, z. B.:
type Values = 1 | 2 | 5
Ist es möglich, eine generische Typ-Ebene-Funktion zu erstellen, die das Maximum extrahiert, d.h.:
type Max<T> = ???
type V = Max<Values>
// V = 5
Antwort #1
TypeScript 4.1 führte Unterstützung für rekursive bedingte Typen ein, sodass der folgende Code direkter geschrieben werden kann als:
type Max<N extends number, A extends any[] = []> =
[N] extends [Partial<A>['length']] ? A['length'] : Max<N, [0, ...A]>;
type Values = 1 | 2 | 5;
type MaxValues = Max<Values>; // 5
und es funktioniert gut genug für Vereinigungen nicht-negativer ganzer Zahlen unter 1000. Es hat immer noch die gleichen Vorbehalte wie unten in Bezug auf unerwartete Eingaben und das Ausbremsen des Compilers, aber einige der Einschränkungen wurden aufgelistet.
Playground-Link zum Code
Antwort für TS4.0:
Die kürzeste illegale (siehe microsoft/TypeScript#26980) Version von Max, die mir einfällt (die variadische Tupel verwendet, die in TS4.0 erscheinen sollen), ist diese:
type MaxIllegal<N extends number, T extends any[] = []> = {
b: T['length'], r: MaxIllegal<N, [0, ...T]>
}[[N] extends [Partial<T>['length']] ? "b" : "r"];
type MaxValuesIllegal = MaxIllegal<Values>; // 5
Diese Art von illegaler Zirkularität funktioniert, bis sie es nicht mehr tut, was ein Grund dafür ist, dass sie nicht unterstützt wird. Wenn man einen Wert eingibt, der keine nicht-negative ganze Zahl ist, rekursiert er, bis ein Compilerfehler auftritt:
type OopsNegative = MaxIllegal<-1 | 3>; // error! Type instantiation is excessively deep and possibly infinite.
Dies kann wahrscheinlich umgangen werden, aber es wird noch schlimmer: Die Rekursion hier endet nach nur wenigen Dutzend Ebenen, sodass man nicht einmal eine Zahl wie 55 verwenden kann, ohne einen Fehler zu bekommen:
type OopsTooBig = MaxIllegal<3 | 55 | 9>; // error! Type instantiation is excessively deep and possibly infinite.
Eine Alternative ist es, programmatisch einen Hilfstyp wie LEQ zu erzeugen, der alle nicht-negativen Ganzzahlen kleiner oder gleich einem gegebenen Schlüssel enthält, bis zu einem von Ihnen wählbaren Maximalwert... hier mache ich 100 oder 99 oder so etwas:
// console.log("type LEQ={"+Array.from({length:100},(_,i)=>i+":"+Array.from({length: i+1},(_,j)=>j).join("|")).join(", ")+"}")
type LEQ = { 0: 0, 1: 0 | 1, 2: 0 | 1 | 2, 3: 0 | 1 | 2 | 3, 4: 0 | 1 | 2 | 3 | 4, 5: 0 | 1 | 2 | 3 | 4 | 5, 6: 0 | 1 | 2 | 3 | 4 | 5 | 6, 7: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7, 8: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8, 9: 0 | 1 | 2 | 3 | 4 | 5 ... SNIP!
Dann können Sie etwas Code schreiben, um die richtigen Werte herauszupicken, ohne illegale Zirkularität zu verwenden:
type Same<T, U, Y = T, N = never> = [T] extends [U] ? [U] extends [T] ? Y : N : N;
type ToLEQ<N extends number> = { [K in keyof LEQ]: [N] extends [Exclude<LEQ[K], K>] ? never : K }[keyof LEQ]
type Max<N extends number, U extends number = ToLEQ<N>> = { [K in keyof LEQ]: Same<U, LEQ[K], K> }[keyof LEQ]
Es funktioniert für dein Beispiel:
type MaxValues = Max<Values>; // 5
Das funktioniert jetzt für 55, weil wir den Stack nicht überlasten:
type NotTooBigAnymore = Max<3 | 55 | 9> // 55
Es funktioniert immer noch nicht, wenn man ihm einen unerwarteten Wert wie -1 gibt, aber hier gibt es einem zumindest nur den maximalen Wert, den es hat, anstatt völlig durchzudrehen:
type OopsNegativeOkayIGuess = Max<-1 | 3> // 99
und wie ich sagte, können Sie es wahrscheinlich umgehen, indem Sie den übergebenen Wert filtern, bevor Sie Max darauf ausführen. Schließlich haben wir einen großen LEQ-Typ herumliegen:
type Max<N extends number, U extends number = ToLEQ<Extract<N, keyof LEQ>>> = { [K in keyof LEQ]: Same<U, LEQ[K], K> }[keyof LEQ]
Und das gibt dir
type OopsNegativeOkayIGuess = Max<-1 | 3> // 3
was zufällig wahr ist (obwohl Max<1 | 2.5> 1 sein wird, was falsch ist).
In jedem Fall treibt dies den Compiler wirklich über alles hinaus, was ich für eine Produktionscodebasis zu empfehlen bereit wäre. Wirklich sollten wir darauf drängen, dass microsoft/TypeScript#26382 implementiert wird, sodass der Compiler einfach auf Typ-Ebene rechnen kann. Daher möchten Sie vielleicht dorthin gehen und ihm ein 👍 geben und möglicherweise Ihren (hoffentlich überzeugenden) Anwendungsfall beschreiben.
Okay, ich hoffe, das hilft; viel Glück!
Playground-Link zum Code
Antwort #2
Oh Junge. Ich habe ein Monster erschaffen, das anscheinend funktioniert, aber aufgrund der Einschränkungen von zirkulären Typen kann es nicht in einem einzigen Schritt gemacht werden.
type DropFirst<T extends TupleLike> = T extends readonly [any, ...infer U] ? U : [...T];
type Longer<T, U extends TupleLike> = U extends [] ? T : T extends [any, ...U] ? T : U;
type TupleLike<T = unknown> = readonly T[] | readonly [T];
type Head<T> = T extends [infer U, ...any[]] ? U : never;
type TupleOf<T, Length extends number, Queue extends any[] = []> = {
done: Queue,
next: TupleOf<T, Length, Prepend<T, Queue>>;
}[Yield<Queue, Length>]
type Yield<Queue extends any[], Length extends number> =
Queue['length'] extends Length
? "done"
: "next"
type Prepend<T, U extends any[]> =
((head: T, ...tail: U) => void) extends ((...all: infer V) => void)
? V
: []
type FindLongestTuple<T extends readonly number[][], Accumulator extends number[] = []> = {
done: Accumulator;
next: FindLongestTuple<DropFirst<T>, Longer<Head<T>, Accumulator>>;
}[T extends [] ? 'done' : 'next'];
type GenerateTuples<T extends readonly number[]> = {
[K in keyof T]: TupleOf<1, T[K] & number>
}
/**
* Usage:
*/
type Input = [4, 5, 6, 1, 2];
type Step1 = GenerateTuples<Input>;
type Step2 = FindLongestTuple<Step1>
type Result = Step2['length']; // 6
Spielplatz
Ich wette, es gibt einen einfacheren Weg, das zu tun. ;-)
