generics - Typescript underklass ärvd metod tillåter odefinierade nycklar i argument


Jag har märkt ett riktigt konstigt beteende när jag försöker ha en underklass med generiska parametrar.

// no error for this code
abstract class BaseClass<Input extends object, Output extends object> {
  public abstract execute(input: Input): Output | void
}

class ChildClass extends BaseClass<{ content: string }, {}> {
  public execute(input: {content: string }){
    return true;
  }
}

Det finns inget kompileringsfel för denna kod. Men jag kan göra innehåll valfritt och TypeScript kommer också att vara nöjd

class ChildClass extends BaseClass<{ content?: string }, {}> {
  public execute(input: {content: string }){
    return true;
  }
}

Jag vet inte om detta beteende är normalt eller inte.

Det som blir riktigt konstigt är att om jag lägger till en slumpmässig nyckel till basklassen, fungerar typkontrollen då som jag skulle förvänta mig:

abstract class BaseClass<Input extends object, Output extends object> {
  // Adding any mandatory key here will do but not an empty object
  public abstract execute(input: Input & { __trustMeImNecessary: true }): Output | void
}

class ChildClass extends BaseClass<{ content?: string }, {}> {
  public execute(input: {content: string }){
    return true;
  }
}

Nu får jag Type '{ content?: string | undefined; } & { __trustMeImNecessary: undefined; }' is not assignable to type '{ content: string; }'. och om jag gör content obligatoriskt kommer det att fungera bra.

Är detta beteende normalt? Kan jag faktiskt använda den här hacken i så fall?

Reproduktion i TypeScript-lekplatsen

Svar #1

Se skillnaden mellan Varians, Kovarians, Kontravarians och Bivarians i TypeScript för relevanta definitioner.

TypeScript kontrollerar metodparametrar bivariant. Det betyder att det accepterar både säkra subklassmetoder som vidgar typerna på sina parametrar (kontravariant) och osäkra subklassmetoder som försämrar typerna på sina parametrar (kovariant). Detta sista beteende är något användbart, men du är inte nöjd med det.


Om du har --strictFunctionTypes-kompilatoralternativet aktiverat kontrolleras icke-metodfunktioner på ett säkert sätt (kontravarianta i sina parametrar). Men det finns inget sätt att tvinga strikt kontroll för metoder. Du kan ändra metoder till funktionsvärda instans-egenskaper och få önskad strikthet:

abstract class BaseClass<I extends object, O extends object> {
  abstract execute: (input: I) => O | void
}

class ChildClass extends BaseClass<{ content?: string }, {}> {
  execute = (input: { content: string }) => true // error
}

class ChildClass2 extends BaseClass<{ content: string }, {}> {
  execute = (input: { content: unknown }) => true // okay
}

Det finns för närvarande inget icke-hackande sätt att använda metoder här. Man kan tänka sig att deklarera superklassens metod som en funktion-värde egenskap och bara skriva över den med metoder, men detta är inte tillåtet. Det finns en funktionsförfrågan på microsoft/TypeScript#51261 för att specifikt tillåta abstract funktion-värde egenskaper att skrivas över med metoder, men för tillfället är det inte en del av språket.

Din hack fungerar genom att göra superklassparametern till en märkt subtyp av den ursprungliga parametertypen I. Underklasser kan enkelt göra det säkra, eftersom vilken supertyp som helst av I är en supertyp av I & {__brand: true}. Men det är nu svårare att göra det osäkra, eftersom du nu skulle behöva hitta en subtyp av I som antingen är en supertyp eller subtyp av I & {__brand: true}. De flesta sätt att göra det innebär att den implementerande metoden använder den märkta nyckeln (__brand här, __trustMeImNecessary i ditt exempel), vilket vi antar är osannolikt. Det kan finnas vissa specialfall, men i första hand gör detta att det beter sig som förväntat.

Lekplatslänk till kod