typescript - Validarea dacă valoarea returnată satisface tipul condițional în semnătura funcției
Încerc să aplic conceptul lui Scott Wlaschin „Programare Orientată pe Căi Ferate” în Typescript.
Încerc să obțin tipurile corecte pentru funcția either. Cred că codul meu de mai jos ar trebui să funcționeze, dar primesc erori de compilare la instrucțiunile return din funcția either.
type Success<T> = {
success: true,
result: T
};
type Failure<E> = {
success: false,
cause: E
}
export function either<T, E, U, F, I extends Success<T> | Failure<E>>(
onSuccess: (i: T) => U,
onFailure: (i: E) => F,
input: I
): I extends Success<T> ? U : I extends Failure<E> ? F : never {
if (input.success === true) {
return onSuccess(input.result); // compiler error: Type 'U' is not assignable to type 'I extends Success<T> ? U : I extends Failure<E> ? F : never'
} else {
return onFailure(input.cause); // compiler error: Type 'F' is not assignable to type 'I extends Success<T> ? U : I extends Failure<E> ? F : never'
}
}
Nu sunt sigur de cauza acestei erori, deoarece tipul condițional indică faptul că pot fi returnate U și F, iar codul meu rezultă în restrângerea corectă a tipului argumentului de intrare. Pot să scap de ele folosind o Asumare de Tip care corespunde tipului de return al funcției (de exemplu return onSuccess(input.result) as I extends Success<T> ? ....), dar întrebările mele sunt:
- Este acesta singurul mod de a scăpa de erorile de compilare?
- Dacă da, de ce nu poate TypeScript să determine că valorile returnate satisfac tipul de return specificat?
Răspuns #1
În prezent, analiza fluxului de control a TypeScript nu afectează parametrii de tip generici. Dacă ai o valoare t de un tip generic T, atunci switch/case sau if/else sau ?/: care protejează tipul lui t poate restrânge tipul aparent al lui t de la T la altceva (de exemplu, T & string), dar nu poate face nimic în privința parametrului de tip T.
Acest lucru are un motiv întemeiat: nu poți pur și simplu să restrângi din nou T. Să spunem că T extends A | B, și verifici isA(t) care restrânge t la A (sau T & A). Aceasta nu implică că T extends A. T ar putea fi A, sau ar putea fi tipul de uniune complet A | B. Am stabilit o nouă limită inferioară pentru T, nu o nouă limită superioară. Poate că asta ar fi scris ca T super A extends A | B, dacă TypeScript ar avea o modalitate de a exprima constrângeri de limită inferioară, așa cum s-a cerut în microsoft/TypeScript#14520. Dar nu are, așa că suntem oarecum blocați, până și dacă aceasta va fi implementată vreodată.
S-ar putea să te gândești că intenția lui T extends A | B este ca T să fie exact unul dintre A sau B, și că uniunile acestor posibilități ar trebui excluse din considerare de la început. Ar fi frumos, dar nu asta înseamnă extends. Am avea nevoie de un nou tip de constrângere pentru asta, așa cum s-a solicitat în microsoft/TypeScript#27808. Să îi spunem oneof. Atunci ai putea scrie T oneof A | B și verificarea isA(t) ar restrânge într-adevăr T la T oneof A. Dar, din nou, acest lucru nu este posibil în prezent, așa că trebuie să așteptăm sau să găsim o soluție alternativă.
De departe cea mai ușoară soluție alternativă este să folosești afirmații de tip pentru a susține că isA(t) implică cu adevărat că T este A.
Dar ai întrebat „este aceasta singura modalitate de a scăpa de erorile compilatorului?” și răspunsul la asta este „nu, dar probabil ar trebui să o faci oricum”.
În acest moment, dacă vrei să folosești generice și să ai tipuri asemănătoare celor dependente fără afirmații de tip, ar trebui să refactorezi astfel încât să nu folosească analiza fluxului de control, ci să efectueze una dintre puținele operațiuni generice pe care TypeScript le poate verifica. În mare parte, acest lucru implică indexarea cu un tip cheie generic într-un anumit tip de obiect sau tipuri mapate pe acel tip de obiect. Abordarea generală este prezentată în microsoft/TypeScript#47109.
Din păcate pentru cazul tău, refactorizarea este destul de proastă și intruzivă. Nu poți indexa obiecte cu valori boolean, așa că ar trebui să schimbăm la un tip string/număr/enum în schimb:
enum Bool { FALSE, TRUE }
const _true = Bool.TRUE;
const _false = Bool.FALSE;
Și apoi ar trebui să rescrii tipurile tale pentru a implica întotdeauna indexarea în tipuri mapate explicite peste obiecte cu aceste chei pseudo-booleene. Poate ceva de genul acesta:
interface BoolMap<T, F> {
[_true]: T,
[_false]: F
}
type Either<T, F, B extends Bool> = { [P in B]:
{ success: P } & BoolMap<{ result: T }, { cause: F }>[P]
}[B]
function either<T, F, TO, FO, B extends Bool>(
onSuccess: (i: T) => TO,
onFailure: (i: F) => FO,
input: Either<T, F, B>
) {
const func: { [P in Bool]:
(i: Either<T, F, P>) => BoolMap<TO, FO>[P]
} = {
[_true]: v => onSuccess(v.result),
[_false]: v => onFailure(v.cause)
};
return func[input.success](input);
}
Pfuu. Dar măcar se compilează fără erori și fără afirmații de tip. Să-l testăm pentru a ne asigura că se comportă în continuare cum ne așteptăm:
const succ = (x: string) => x.length;
const fail = (x: number) => x.toFixed(2);
const r1 = either(succ, fail, { success: _true, result: "abc" });
// const r1: number
const r2 = either(succ, fail, { success: _false, cause: 404 });
// const r2: string
const r3 = either(succ, fail,
Math.random() < 0.5 ?
{ success: _true, result: "abc" } :
{ success: _false, cause: 404 }
);
// const r3: string | number
Arată bine, deci cel puțin apelanții lui either au aceeași experiență, uh, dacă ignori chestia cu booleanul.
Așa. Personal, probabil aș rămâne doar la afirmațiile de tip și poate, într-o versiune viitoare de TypeScript, când analiza fluxului de control va afecta parametrii de tip generici, le vei putea elimina.
Link de teren de joacă către cod
