Strängare snävning i TypeScript som endast är tillåten om det snäva fallet är möjligt


I följande kodsnutt

interface C1 { kind: 'c1' }
interface C2 { kind: 'c2' }
interface C3<T> { kind: 'c3'; value: T }

function isC3<T, CX extends ([C3<T>] extends [CX] ? { kind: string; } : never)>(
    c: CX
): c is C3<T> /* <------- PROBLEM !!! */ {
    return c.kind === 'c3';
}

// valid use
type C = C1 | C2 | C3<number>;
isC3<number, C>({ kind: 'c3', value: 1 }); // true
isC3<number, C>({ kind: 'c1' });           // false


// invalid use
type D = C1 | C2;
declare var d: D;
isC3<number, D>(d); // D doesn't satisfy constraint never (as expected)

Jag försöker skapa isC3-typvaktfunktionen på ett sätt så att ett anrop till den bara kompileras om typen av den givna typargumenten har C3-fallet i sig, annars vill jag få ett kompileringsfel (vänligen titta på exemplen)

Så allt fungerar, förutom att jag får ett fel på typvaktens returtyp:

A type predicate's type must be assignable to its parameter's type.
  Type 'C3<T>' is not assignable to type 'CX'.
    'C3<T>' is assignable to the constraint of type 'CX', but 'CX' could be instantiated with a different subtype of constraint '{ kind: string; }'.ts(2677)

Finns det ett sätt att få det jag letar efter?

Svar #1

Om du har en typ T som du vet är tilldelningsbar till U men kompilatorn inte vet detta, och du behöver använda T på ett ställe där kompilatorn förväntar sig något som är tilldelningsbart till U, kan du antingen använda hjälpverktyget Extract<T, U> eller eventuellt Extract<U, T>, eller så kan du använda snittet T & U, beroende på ditt användningsfall:

declare function isC3<T, CX extends ([C3<T>] extends [CX] ? { kind: string; } : never)>(
    c: CX
): c is Extract<CX, C3<T>> // okay

eller

function isC3<T, CX extends ([C3<T>] extends [CX] ? { kind: string; } : never)>(
    c: CX
): c is CX & C3<T> {
    return c.kind === 'c3';
}

Båda dessa resulterar i slutändan i en typ som effektivt är C3<T>. Extract<T, U> filtrerar en union i T till bara de medlemmar som kan tilldelas till U (så om du förväntar dig att CX är en union, är Extract<CX, C3<T>> rimligt), medan T & U är mer agnostisk när det gäller unioner.

Lekplatslänk till kod