programare funcțională - Există o modalitate mai bună de a conecta funcțiile Type Guard în TypeScript?


M-am gândit la următoarele pentru a scrie o funcție pipe pentru Type Guards:

type PipedTypeGuard<A, B> = (
    value: A,
    ...args: readonly unknown[]
) => value is B extends A ? B : never;
type GuardPipeFn =  <A, B, C, D, E, F, G, H, I, J, K>(
    guard1: PipedTypeGuard<unknown, A>,
    guard2?: PipedTypeGuard<A, B>,
    guard3?: PipedTypeGuard<B, C>,
    guard5?: PipedTypeGuard<C, D>,
    guard6?: PipedTypeGuard<D, E>,
    guard7?: PipedTypeGuard<E, F>,
    guard8?: PipedTypeGuard<F, G>,
    guard9?: PipedTypeGuard<G, H>,
    guard10?: PipedTypeGuard<H, I>,
    guard11?: PipedTypeGuard<I, J>,
    guard12?: PipedTypeGuard<J, K>
) => (value: unknown) => value is A & B & C & D & E & F & G & H & I & J & K;

const guardPipe: GuardPipeFn = ()=>'not implemented'
// usage
const isFoobar = guardPipe(
     (val): val is string => typeof val === 'string'
     (val): val is `foo${string}` => val.startsWith('foo'), // (parameter) val: string
     (val): val is `foobar` => val === 'foobar' // (parameter) val: `foo${string}`
);
const test = {} as unknown;
if (isFoobar(test)) {
    test; // "foobar"
}

Link către terenul de joacă

Acest lucru funcționează, dar permite doar un număr limitat de parametri. De asemenea, se simte puțin redundant să scriu asta. Există o modalitate mai bună, de exemplu recursivă, de a face acest lucru? Funcționalitatea principală pe care încerc să o obțin este ca Tipul de Gărdă al primului să fie transmis parametrului următor și așa mai departe.

__ Unele lucruri pe care le-am încercat:

/**
 * A type that a Guard will assign to a variable.
 * @example
 * ```ts
 * GuardType<typeof isString> // string
 * GuardType<typeof Array.isArray> // any[]
 * ```
 */
declare type GuardType<Guard extends any> = Guard extends (
    value: unknown,
    ...args: readonly any[]
) => value is infer U
    ? U
    : never;

// this will only work to infer the returned Guard Type of a array of Type Guards, but can not assign to individual parameters
type CombineGuardType<
    Arr extends ReadonlyArray<AnyTypeGuard>,
    Result = unknown
> = Arr extends readonly []
    ? Result
    : Arr extends readonly [infer Head, ...infer Tail]
    ? Tail extends readonly AnyTypeGuard[]
        ? CombineGuardType<Tail, Result & GuardType<Head>>
        : never
    : never;


Răspuns #1

Algoritmul de inferență a tipurilor din TypeScript este, în esență, o serie de euristici care funcționează destul de bine într-o varietate largă de scenarii, dar are limitări. De fiecare dată când scrii cod în care ai nevoie ca compilatorul să inferențeze atât parametrii de tip generici, cât și parametrii de callback neannotați contextual în același timp, există o mare șansă să întâlnești astfel de limitări.

Vezi microsoft/TypeScript#25826 pentru un exemplu de acest tip de problemă. Este teoretic posibil să se implementeze un algoritm de unificare mai riguros, așa cum se discută în microsoft/TypeScript#38872, dar este puțin probabil să se întâmple în viitorul apropiat.


Versiunea codului pe care o ai, cu mulți parametri de tip distincti, funcționează pentru că permite inferența de la stânga la dreapta din parametrii funcției distincți. Dar orice fel de abstractizare către un singur parametru de tip asemănător unui array înseamnă că trebuie să faci inferența dintr-un tuple rest, și lucrurile nu se comportă la fel de bine. De exemplu, ai putea rescrie într-o versiune variadică următoare, unde parametrul de tip T corespunde array-ului de tipuri protejate, deci T corespunde cu ceva de genul [A, B, C, D, ...]:

type Idx<T, K> = K extends keyof T ? T[K] : any;
type Prev<T extends any[], I extends keyof T> = Idx<[any, ...T], I>
type Last<T extends any[]> = T extends readonly [...infer _, infer L] ? L : never;
type Guards<T extends any[]> = 
  { [I in keyof T]: (val: Prev<T, I>) => val is Extract<T[I], Prev<T, I>> }
function guardPipe<T extends unknown[]>(
  ...args: [...Guards<T>]): (val: any) => val is Last<T>;
function guardPipe(...args: ((val: any) => boolean)[]) {
    return (val: any) => args.every(p => p(val));
}

ceea ce funcționează bine atâta timp cât eliberezi compilatorul de necesitatea de a deduce tipurile parametrilor callback-ului:

const p = guardPipe(
    (x: any): x is { a: string } => ("a" in x) && (typeof x.a === "string"),
    (x: { a: string }): x is { a: string, b: number } => 
      ("b" in x) && (typeof (x as any).b === "number"),
    (x: { a: string, b: number }): 
      x is { a: "hello", b: number } => x.a === "hello"
);

/* const p: (val: any) => val is {
    a: "hello";
    b: number;
} */


const val = Math.random() < 1000 ? { a: "hello", b: Math.PI } : { a: "goodbye", c: 123 };

if (p(val)) {
    console.log(val.b.toFixed(2)) // 3.14
}

și detectează erorile atunci când lanțul tău de tipuri nu devine din ce în ce mai restrâns:

const caughtError = guardPipe(
    (x: { a: string }): x is { a: string, b: number } => 
      ("b" in x) && (typeof (x as any).b === "number"),
    (x: any): x is { a: string } => ("a" in x) && (typeof x.a === "string"), // error!
    // Type predicate 'x is { a: string; }' is not assignable to 'val is never'.
    (x: { a: string, b: number }): 
      x is { a: "hello", b: number } => x.a === "hello"
)

dar are probleme imediat ce nu annotați parametrii callback-ului:

const oops= guardPipe(
    (x): x is { a: string } => ("a" in x) && (typeof x.a === "string"),
    (x): x is { a: string, b: number } => 
      ("b" in x) && (typeof (x as any).b === "number"),
    (x): x is { a: "hello", b: number } => x.a === "hello"
);
// const oops: (val: any) => val is never 
// uh oh

Aici parametrul generic de tip T eșuează complet să fie dedus, revine la unknown[] și produce un gard de tip (val: any) => val is never. Blah. Deci aceasta este mai rău decât versiunea ta.


În absența unui algoritm de inferență a tipului mai robust, ar fi mai bine să profiți de punctele forte ale compilatorului în loc de slăbiciunile sale dacă vrei o versiune cu adevărat generală a guardPipe(). De exemplu, poți refactoriza de la o singură funcție variadică la o funcție curry sau la o interfață fluentă, unde fiecare apel de funcție/metodă necesită doar inferența unui singur argument de callback și a unui singur parametru de tip:

type GuardPipe<T> = {
    guard: (val: unknown) => val is T;
    and<U extends T>(guard: (val: T) => val is U): GuardPipe<U>;
}
function guardPipe<T>(guard: (val: any) => val is T): GuardPipe<T>;
function guardPipe(guard: (val: any) => boolean) {

    function guardPipeInner(
      prevGuard: (val: any) => boolean, 
      curGuard: (val: any) => boolean
    ) {
        const combinedGuard = (val: any) => prevGuard(val) && curGuard(val);
        return {
            guard: combinedGuard,
            and: (nextGuard: (val: any) => boolean) =>
              guardPipeInner(combinedGuard, nextGuard)
        }
    }
    return guardPipeInner(x => true, guard) as any;
}

Aici, dacă guard este de tip (val: any) => val is T, atunci apelul către guardPipe(guard) produce o valoare de tip GuardPipe<T>. Un GuardPipe<T> poate fi folosit fie direct ca un guard de acel tip apelând metoda sa guard, fie poți lega un nou guard la final prin metoda sa and. Exemplul de mai înainte devine astfel:

const p = guardPipe(
    (x): x is { a: string } => x && ("a" in x) && (typeof x.a === "string")
).and((x): x is { a: string, b: number } => 
  ("b" in x) && (typeof (x as any).b === "number")
).and((x): x is { a: "hello", b: number } => x.a === "hello"
).guard;

const val = Math.random() < 1000 ? { a: "hello", b: Math.PI } : { a: "goodbye", c: 123 };

if (p(val)) {
    console.log(val.b.toFixed(2)) // 3.14
}

const oops = guardPipe(
    (x): x is { a: string, b: number } => ("b" in x) && (typeof (x as any).b === "number")
).and(
    (x): x is { a: string } => x && ("a" in x) && (typeof x.a === "string") // error!
    //  Type '{ a: string; }' is not assignable to type '{ a: string; b: number; }'
).and((x): x is { a: "hello", b: number } => x.a === "hello"
).guard;

care este destul de similar și are avantajul de a permite compilatorului să deducă cu acuratețe tipurile parametrilor de tip fără a te obliga să adnoti toți acei parametri de callback.

Link de teren de joacă către cod