typescript - Utvidelse av grensesnitt med generisk er ikke lenger tildelbar til overordnet
Jeg oppgraderte nylig til TypeScript 2.4, og jeg fikk flere feil som klaget over at typene mine ikke lenger kunne tilordnes.
Her er scenariet hvor jeg støter på feilen:
interface Parent {
prop: any
}
interface Child extends Parent {
childProp: any
}
type Foo<T> = <P extends Parent>(parent: P) => T
function createFooFunction<T>(arg: T): Foo<T> {
// Error here!
return (child: Child): T => {
return arg;
}
}
I TypeScript 2.3 er dette akseptabelt, men TypeScript 2.4 gir denne feilen
Type '(child: Child) => T' is not assignable to type 'Foo<T>'.
Types of parameters 'child' and 'parent' are incompatible.
Type 'P' is not assignable to type 'Child'.
Type 'Parent' is not assignable to type 'Child'.
Property 'childProp' is missing in type 'Parent'.
Når det gjelder den siste linjen i feilen, la jeg merke til at hvis jeg gjør barnets egenskaper valgfrie, vil TypeScript bli fornøyd, dvs. hvis jeg gjør denne endringen
interface Child extends Parent {
childProp?: any
}
Selv om det ikke er en ideell løsning, siden childProp er nødvendig i mitt tilfelle.
Jeg la også merke til at det å endre Foos argumenttype direkte til Parent også vil tilfredsstille TypeScript, altså å gjøre denne endringen
type Foo<T> = (parent: Parent) => T
Dette er heller ikke en løsning, siden jeg ikke kontrollerer Foo-typen, eller Parent. De kommer begge fra leverandørens .d-filer, så jeg kan ikke endre dem.
Men uansett, jeg er ikke sikker på at jeg forstår hvorfor dette er en feil. Foo-typen sier at den krever noe som utvider Parent, og Child er et slikt objekt, så hvorfor skulle TypeScript tro at det ikke kan tildeles?
Rediger: Jeg har merket dette som besvart siden det å legge til --noStrictGenericChecks-flagget vil undertrykke feilene (akseptert svar her). Jeg vil imidlertid fortsatt gjerne vite hvorfor dette er en feil i utgangspunktet, siden jeg heller vil beholde de strenge kontrollene og refaktorere koden min hvis den er feil, i stedet for bare å omgå det.
Så for å gjenta kjernen i spørsmålet, siden Child utvider Parent, hvorfor mener ikke TypeScript lenger at Child kan tilordnes til Parent, og når det gjelder OOP-generics, hvorfor er det mer korrekt nå enn det var før?
Svar #1
Jeg tror problemet er at du returnerer en funksjon som krever en Child som argument, men tilordner den til en funksjon som må kunne ta alt som avledes fra Parent, ikke bare Child.
La oss si at du kaller createFooFunction<T> en get Foo<T>. Ifølge definisjonen av Foo<T> er det mulig å lage en klasse Child2 som utvider Parent og sende den som et argument til Foo<T> som du fikk. Dette er et problem siden du faktisk returnerte en funksjon som bare kan motta Child som argument, aldri Child2.
Dette er faktisk mer korrekt. Husk at du ikke tar en Child og tilordner den til Parent. Du tar en funksjon som kun forventer en Child og tilordner den til en som kan ta hvilken som helst type Parent. Dette er veldig forskjellig. Hvis vi derimot hadde hatt med en returverdi å gjøre og ikke en input, ville dette vært fint. Dette er forskjellen mellom kovarians og kontravarians, som kan være litt forvirrende i begynnelsen. I C# ville du kunne spesifisere dette i dine egne generiske klasser ved å bruke in og out nøkkelord.
I C#, for eksempel, hvis du hadde en IEnumerable<Child> (deklarert som IEnumerable<out T>), er det også naturlig en IEnumerable<Parent>, siden du itererer - du får Child-objekter ut - du kan også få Parent-objekter ut, fordi hver Child også er en Parent. Derfor kan du tilordne IEnumerable<Child> til IEnumerable<Parent>, men ikke omvendt! Siden du ikke ville være garantert å få et Child-objekt.
På den annen side, hvis du hadde noe som IComparer<Parent> (deklarert som IComparer<in T>) som kan sammenligne to Parent-objekter - vel, siden hver Child også er en Parent, kan det også sammenligne to Child-objekter. Derfor kan du tilordne IComparer<Parent> til IComparer<Child>, men ikke motsatt - noe som kun kan sammenligne en Child vet bare hvordan man sammenligner en Child! Det er problemet ditt her.
Du kan forstå in og out som inngangs- (argumenter) og utgangsverdier (retur) i tilbakeringingsfunksjoner. Du kan bare gjøre innganger mer spesifikke (kontravarans) og utganger mer generiske (kovarians).
Forresten, jeg mener <P extends Parent> er helt unyttig her (og det bidrar til forvirringen), fordi du kunne passere hva som helst som utvider Parent i den funksjonen uansett, selv om den ikke var generisk. Dette ville bare vært nyttig hvis du returnerte typen på denne måten: <P extends Parent>(parent: P) => P for å bevare korrekt type P i returverdien. Uten generics, måtte du ta Parent og returnere Parent, så du ville få tilbake en Parent selv om du satte inn noe mer spesifikt. Når det gjelder hvorfor feilen forsvinner hvis du kvitter deg med dette, har jeg virkelig ingen anelse.
Svar #2
Versjon 2.4 introduserer strengere kontroll for generiske typer
Les dette https://blogs.msdn.microsoft.com/typescript/2017/06/27/announcing-typescript-2-4/ og se etter overskriften Strengere sjekking for generics
Fra artikkelen:
Som en midlertidig løsning for eventuelle problemer, kan du kanskje undertrykke noen av disse feilene ved å bruke den nye
--noStrictGenericChecks-flagget.
Det introduserer også streng kontravarians for tilbakeringinger.