Typescript-generisk med begrensning har ikke nøkkel


Jeg prøver å lage et sett med funksjoner som opererer på en generisk T som må implementere et grensesnitt IDocument. Selv om dette generelt ser ut til å fungere, ser det ut som om TypeScript ikke gjenkjenner at T må ha nøklene til IDocument.

Her er et minimalt eksempel:

interface IDocument
{
    _id: number;
}

function testGeneric<T>(v: { [P in keyof T]?: T[P] })
{ }

function testConstraint<T extends IDocument>(doc: T)
{
    // this works
    console.log(doc._id);

    // this works
    testGeneric<IDocument>({ _id: doc._id });

    // this fails
    // Argument of type '{ _id: number; }' is not assignable to parameter of type '{ [P in keyof T]?: T[P] | undefined; }'.
    testGeneric<T>({ _id: doc._id });
}

Du kan se dette live på TypeScript-lekeplassen her.

Jeg er forvirret over hvorfor dette ikke fungerer siden det virker som om i min testConstraint-funksjon vil T alltid ha en _id-nøkkel siden det må implementere IDocument. Og faktisk, hvis jeg tar en T-parameter og får tilgang til _id-egenskapen, fungerer det fint.

Merk at testGeneric-funksjonen er i et bibliotek som jeg ikke eier, så jeg kan ikke endre den signaturen.

Hva gjør jeg galt her? Må jeg bruke en annen begrensning for å uttrykke at T må ha hver nøkkel som IDocument har?

Svar #1

Det originale eksemplet hadde en verdi som {_id: 10} tildelt en generisk type som Partial<T> hvor T extends IDocument. I slike tilfeller kan du vise at kompilatoren har rett i å klage, siden det finnes noen typer T som utvider IDocument hvor 10 ikke er en gyldig egenskap. Dette er egentlig det samme problemet som i dette spørsmålet.

Det nye eksemplet hvor du tilordner {_id: doc._id} til den generiske typen Partial<T> hvor T extends IDocument gir fortsatt en feil, selv om det definitivt burde være trygt. Kompilatoren er ikke i stand til å verifisere at Pick<T, "_id"> kan tilordnes til Partial<T>. Dette er (relatert til) et åpent problem; kompilatoren kan foreløpig ikke utføre den typen typeanalyse som er nødvendig for å sikre dette. I situasjoner hvor du er sikker på at noe er trygt (og du har sjekket det grundig flere ganger), men kompilatoren ikke er det, kan du vurdere å bruke en typepåstand:

testGeneric<T>({ _id: doc._id } as Partial<T>); // okay now

Så inne i implementeringen av testConstraint() kan det hende du må bruke en typepåstand eller det tilsvarende (som for eksempel en enkelt funksjonssignatur-overload med en løsere implementeringssignatur).

Endelig sa du at du faktisk ønsket å hindre noen i å kalle testConstraint<T>() med en T hvis egenskaper er smalere enn de på IDocument. Dette er mer restriktivt enn T extends IDocument, og er mer tungvint å representere i TypeScript hvor egenskapssmaling er en naturlig del av subtypingen. Du kan gjøre det ved å la den generiske begrensningen inkludere en betinget kartlagt type, slik:

function testConstraint<
    T extends IDocument &
    { [K in keyof IDocument]: IDocument[K] extends T[K] ? T[K] : never }>(doc: T): void;
function testConstraint(doc: IDocument) {
    testGeneric({ _id: doc._id });
}

Her har vi T begrenset til både IDocument og en type hvor hver egenskap av IDocument blir sammenlignet med den tilsvarende egenskapen av T. Hvis den på T ikke er smalere enn den på IDocument, flott. Ellers blir egenskapen i begrensningen innsnevret helt til never, som T sannsynligvis ikke vil matche.

Kallesignaturen er komplisert nok til at typene inne i implementasjonen virkelig ville forvirre kompilatoren. Derfor gjorde jeg det til en overload, og løsket implementasjonsignaturen slik at den er helt ikke-generisk. Du kan kanskje gjøre noe generisk for implementasjonen, men poenget er at du sannsynligvis bør behandle kald-siden og implementasjonen separat når det gjelder typer.

La oss se den funksjonen i aksjon:

interface TheVeryFirstDocument extends IDocument {
    _id: 1
}
declare const tv1d: TheVeryFirstDocument;
testConstraint(tv1d); // error!
// --------->  ~~~~~
//  Types of property '_id' are incompatible.
//    Type '1' is not assignable to type 'never'.(

Det gir en feil, som vi ønsker, mens følgende fungerer uten feil:

declare const doc: IDocument;
testConstraint(doc); // okay

interface ExtendedDocument extends IDocument {
    title: string;
    numPages: number;
}
declare const xDoc: ExtendedDocument;
testConstraint(xDoc); // okay

Ok, håper det hjelper; lykke til! Link til kode