TypeScript this i ett gränssnitt bör referera till sig själv och barn, inte bara sig själv
Här är exemplet:
interface A {
method: (itself: this) => string;
}
interface B extends A {
additionalProperty: boolean;
}
type Extend<T extends A> = A & { extension: number };
type ExtendedA = Extend<A>
type ExtendedB = Extend<B>
Sandlådan.
När jag försöker utöka B skriver TypeScript:
Typen 'B' uppfyller inte begränsningen 'A'. Typerna för egenskapen 'method' är inkompatibla. Typen '(itself: B) => string' kan inte tilldelas typen '(itself: A) => string'. Typerna för parametrarna 'itself' och 'itself' är inkompatibla. Egenskapen 'additionalProperty' saknas i typen 'A' men krävs i typen 'B'.(2344) input.tsx(6, 2): 'additionalProperty' deklareras här.
Men B utökar A. De borde vara kompatibla.
UPPDATERING #1:
Jag kan inte förklara detta, men det verkar som att om jag ersätter gränssnitt med klasser fungerar typningen perfekt.
UPPDATERING #2:
Tja, det fungerar bara med klassmetoder, men det fungerar inte, till exempel, med pilfunktioner. Fortfarande märkligt.
UPPDATERING #3
Om gränssnittet definieras på följande sätt fungerar det inte:
interface A {
method: (itself: this) => string;
}
Men om det definieras på följande sätt fungerar det:
interface A {
method(itself: this): string;
}
Det har ingen mening alls. Men när jag letade efter orsaken till detta beteende hittade jag detta utmärkta svar. Det gav mig en ledtråd till orsakerna till denna skillnad.
Det nämndes TypeScript-alternativet strictFunctionTypes.
När den är aktiverad gör denna flagga att funktionsparametrar kontrolleras mer korrekt.
Under utvecklingen av denna funktion upptäckte vi ett stort antal i grunden osäkra klasshierarkier, inklusive några i DOM. På grund av detta gäller inställningen endast för funktioner skrivna i funktionssyntax, inte för de i metoddsyntax.
Det förklarar orsaken till den märkliga skillnaden i beteende. Jag kan bara stänga av detta alternativ, men det känns som en tillfällig lösning.
Jag behöver fortfarande en annan lösning.
UPPDATERING #4
Jag antar att detta fel är avsett att förhindra sådana osäkra tilldelningar:
const a: A = {
method(b: B) {
return `${b.toString()} / ${b.additionalProperty}`;
}
}
Men sådana typer av fel är inte specifika för mitt fall.
UPPDATERING #5
Jag har hittat en annan lösning
type Method<T extends (...args: any) => any> = {
f(...args: Parameters<T>): ReturnType<T>;
}['f'];
interface A {
method: Method<(itself: this) => string>;
}
interface B extends A {
additionalProperty: boolean;
}
type Extend<T extends A> = T & { extension: number };
type ExtendedA = Extend<A>
type ExtendedB = Extend<B>
Kontrollera det själv. Det är bättre än att inaktivera strictFunctionTypes, men det är fortfarande en tillfällig lösning.
Svar #1
Nyckelordet this är beroende av det sammanhang där det definieras. När det definieras i interface A refererar det till A, och i interface B refererar det till B
Därför blir de båda inkompatibla när man kontrollerar extends-begränsningen.
Lösning 1 :
Så vi kan antingen definiera dem uttryckligen separat eller bara lägga till en union av dem A | B
interface A {
method: (itself: A | B) => string;
}
interface B extends A {
additionalProperty: boolean;
}
type Extend<T extends A> = T & { extension: number };
type ExtendedA = Extend<A>
type ExtendedB = Extend<B>
Kodlekplats
Lösning 2 :
Vi kan göra method<T>(): void till en generisk funktion så att context-relaterad tvetydighet för this löses.
interface A {
method: <T> (itself: T) => void;
}
interface B extends A {
additionalProperty: boolean;
}
type Extend<T extends A> = T & { extension: number };
type ExtendedA = Extend<A>
type ExtendedB = Extend<B>
const b: B = {
method<B>() {
console.log(this.additionalProperty)
},
additionalProperty: true
}
Kodlekplats Lösning 2
Svar #2
Sammanfattning
Detta problem uppstod endast om strictFunctionTypes-flaggan är på (det är standard i strikt läge, så det är ett vanligt fall). Under denna flagga kontrolleras funktionsparametrarnas typer kontravariant istället för bivariant.
I det här fallet: (arg: B) => void kan inte tilldelas (arg: A) => void även om B extends A,
Lösningar:
a) Använd metoder istället för egenskaper om det är möjligt. Typparameterns positioner i metoderna kontrolleras bivariant.
b) Använd bivarianshak för att tvinga TypeScript att kontrollera typparameterpositioner bivarians.
type Bivariant<T extends (...args: any) => any> = {
f(...args: Parameters<T>): ReturnType<T>;
}['f'];
let callback: Bivariant<(event: E) => void>
c) Inaktivera strictFunctionTypes-flaggan. Men det kommer inte att hjälpa de andra användare som kommer att använda din kod, så det är knappt en lösning.
Roten till problemet.
Igen uppstod detta problem endast om strictFunctionTypes-flaggan är på.
Under denna flagga kontrolleras typpositioner för funktionsparametrar kontravarirant istället för bivarirant.
Dessa konstiga ord borde inte besvära dig. Jag kommer inte att dyka in i typteorin (men jag rekommenderar att du läser detta utmärkta svar).
Låt oss säga B extends A, C extends B, D extends C och så vidare. Då:
I frågan har vi problemet med att tilldela B till A eftersom dess metoder inte är jämförbara. Om C extends B, då om parameterpositioner kontrolleras kontravariantiskt är method(B) inte jämförbar med method(C) (titta på bilden).
Varför behöver jag strictFunctionTypes från första början?
Låt oss titta på motivationsexemplet. Först och främst, inaktivera flaggan strictFunctionTypes.
interface Article {
content: string;
}
interface TitledArticle extends Article {
title: string;
}
let printer: (article: Article) => string;
Låt oss skapa skrivaren för den titulerade artikeln:
let printer = (article: TitledArticle) => {
return `${article.title.toUpperCase()} / ${article.content}`
}
printer({ content: "Great Answer" }); // exception
Vi har fått ett undantag, men TypeScript varnade oss inte!
Tja, det kommer att varna om du aktiverar strictFunctionTypes. Det är poängen.
Än en gång: det är farligt att tilldela funktionen (arg: B) => void till (arg: A) => void, eftersom du efter en sådan tilldelning fortfarande kommer att kunna skicka A till denna funktion och det kan leda till olika problem, eftersom A inte har alla egenskaper som B har. Det är därför denna regel behövs.
Varför behöver jag någonsin kontrollera positionsparametrar för funktionstyp bivariat?
Låt oss skapa den anpassade CustomArray:
class CustomArray<T> {
// it's not method, it's a property and it's important!
push = (item: T) => {
// something
}
}
let a = new CustomArray<number>();
let b = new CustomArray<0>();
a = b; // typescript error
Type 'CustomArray<0>' is not assignable to type 'CustomArray<number>'.
Type 'number' is not assignable to type '0'.
Jag antar att samma problem uppstod med standard Array. Det är därför Typescript-utvecklarna lämnar bivariat kontroll av funktionstypens parameterposition för konstruktorer och metoder, även om strictFunctionTypes är på.
Under utvecklingen av denna funktion upptäckte vi ett stort antal i grunden osäkra klasshierarkier, inklusive några i DOM. På grund av detta gäller inställningen endast för funktioner skrivna i funktionssyntax, inte för de i metoddsyntax.
Du kan kontrollera det själv, genom att ersätta egenskapen push med en metod:
class CustomArray<T> {
push(item: T) {
// something
}
}
let a = new CustomArray<number>();
let b = new CustomArray<0>();
a = b; // no error
Så, du kan lita på denna egenskap och använda metoder. Men det finns situationer där metoder helt enkelt inte är tillräckligt bra för dig.
Det finns exempel på typer där du inte kan använda metoder
interface A {
method?: (a: this) => void;
}
interface A {
method: ((a: this) => number) | number;
}
const callback = (e: Event) => void;
Vad kan du göra i sådana situationer?
Bivarians-hack
Det finns ett knep som kan hjälpa dig att tvinga TypeScript att kontrollera funktionstypens parameterpositioner bivariant.
Idén är enkel: om strictFunctionTypes inte gäller för metoder, låt oss göra funktionen till en metod.
let callback: (event: E) => void
let callback: {'method': (event: E) => void}['method']
Det är så enkelt men det fungerar!
Låt oss skriva den generiska typen:
type Bivariant<T extends (...args: any) => any> = {
f(...args: Parameters<T>): ReturnType<T>;
}['f'];
let callback: Bivariant<(event: E) => void>
Det är fortfarande hack, men det är ännu trevligt nu.
Om du använder det, oroa dig inte. Du är inte ensam. React, Material UI och många andra använder det. Du är i gott sällskap, använd det bara klokt, för med stor makt följer stort ansvar (tja, på ett tråkigt sätt minskar det typsäkerheten).
