TypeScript feil typeanelse når klasse implementerer grensesnitt
Når jeg bruker komposisjonsmetoden i TypeScript fremfor arv, ønsker jeg å beskrive mine enheter i henhold til hva de «kan», og ikke hva de «er». For å gjøre dette må jeg lage noen komplekse grensesnitt, og deretter implementere grensesnittene mine for klassene mine (jeg bruker klasser for å unngå å lage en manuell prototypekjede og for ikke å bryte noen optimaliseringer som jeg antar eksisterer i js-motorer). Men dette resulterer i merkelig oppførsel når typen til en metode ikke blir korrekt utledet. Derimot, når jeg bruker objekter og erklærer dem som av samme grensesnittstype, fungerer alt som forventet.
Så jeg bruker VSCode med TypeScript 3.6.3. Jeg har laget et grensesnitt for 2D-form som skal ha en metode for å returnere alle normalene til kanter. Deretter lager jeg en klasse som implementerer det grensesnittet, og jeg forventer at det skal kreve denne metoden og at den skal ha samme returtype (den delen fungerer) og også samme argumenttyper (denne fungerer ikke). Parameteren blir inferert som any. Problemet mitt er at jeg ikke vil lage prototypekjede for hånd bare for å få konsistent VSCode-adferd.
Også når jeg kjører tsc i konsollen får jeg samme feilen for parameter som er av typen 'any' i klassemmetode og forventet feil inne i objektmetode når jeg prøver å få tilgang til en ikke-eksisterende egenskap
interface _IVector2 {
x: number;
y: number;
}
interface _IShape2D {
getNormals: ( v: string ) => _IVector2[];
}
export class Shape2D implements _IShape2D {
getNormals( v ) {
console.log( v.g );
^ ----- no error here
return [{} as _IVector2];
}
}
export const Test: _IShape2D = {
getNormals( v ) {
console.log( v.g );
^------ here we get expected error that
^------ 'g doesn`t exist on type string'
return [{} as _IVector2];
}
};
min tsconfig.json
{
"compilerOptions": {
"target": "es2017",
"allowSyntheticDefaultImports": true,
"checkJs": false,
"allowJs": true,
"noEmit": true,
"baseUrl": ".",
"moduleResolution": "node",
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true,
"jsx": "react",
"module": "commonjs",
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"noErrorTruncation": true,
"removeComments": true,
"resolveJsonModule": true,
"sourceMap": true,
"watch": true,
"skipLibCheck": true,
"paths": {
"@s/*": ["./src/*"],
"@i/*": ["./src/internal/*"]
}
},
"exclude": [
"node_modules"
]
}
Forventet:
- klassens metodeparameter skal antas å være streng
Faktisk:
- metodens parameter antas å være hvilken som helst
Til syvende og sist er spørsmålet mitt som følger: "Er denne oppførselen uoppnåelig i ts, og bør jeg ty til håndskrevne (oi nei...) prototypekjeder og enkle objekter for prototyper?"
Takk på forhånd!
Svar #1
Dette er en designbegrensning i TypeScript (se ms/TS#1373). Det ble forsøkt en løsning på ms/TS#6118, men den hadde noen dårlige/brytende interaksjoner med eksisterende kode i den virkelige verden, så de ga opp på det. Det er en åpen sak på ms/TS#32082 som ber om noe bedre, men foreløpig er det ingenting nyttig.
Forslaget på dette stadiet er å manuelt annotere parametertyper i implementerende/utvidende klasser; dette er bedre enn å ty til håndskrevne prototypekjeder, til tross for at det er mer irriterende.
export class Shape2D implements _IShape2D {
getNormals(v: string) { // annotate here
console.log(v.g); // <-- error here as expected
return [{} as _IVector2];
}
}
Merk at v faktisk kan være any eller unknown, og getNormals() ville være en korrekt implementering:
export class WeirdShape implements _IShape2D {
getNormals(v: unknown) { // okay
return [];
}
}
Dette skyldes at metodeparameterkontravarians er typesikker... en WeirdShape er fortsatt en helt gyldig _IShape2D. Så, selv om det ville vært fint om parameteren ble antatt å være string, er det ingenting feil med at den er mer generell.
Uansett, håper det hjelper; lykke til!
Lenke til kode
Svar #2
Som en løsning kan du bruke Parameters-verktøystypen for å hente ut parameterne fra et annet grensesnitt. Dette er nyttig når du bruker ett enkelt objekt som en funksjonsparameter (i stedet for posisjonsparametere) og du har metoder hvis argumenter kan endre seg ofte.
For eksempel:
interface Foo {
method(args: { arg1: string, arg2: number}): void;
}
class Bar implements Foo {
method({ arg1, arg2 }: Parameters<Foo['method']>[0]): void {
// arg1 and arg2 types will be correctly inferred
}
}
Hvis du har flere metoder for å erklære på nytt, kan du lage din egen nyttige type for en bestemt klasse:
interface Foo {
method(args: { arg1: string, arg2: number}): void;
}
type FooArgs<Method extends keyof Foo> = Parameters<Foo[Method]>[0];
class Bar implements Foo {
method({ arg1, arg2 }: FooArgs<'method'>): void {
// arg1 and arg2 types will be correctly inferred
}
}
Håper det hjelper.
