Typescript generisk begränsning som använder härledd metodreturtyp


jag har detta scenario:

abstract class AbstractClass<T> {
  abstract getData(): T;
  getBase(): Partial<T> {
    return {};
  }
}

interface Contract {
  prop1: string;
  prop2: string;
}

class Impl extends AbstractClass<Contract> {
  get prop1() {
    // some complex logic here
    return '';
  }

  getBase() {
    return {
      prop2: 'foo'
    }
  }
}

hur uttrycker jag begränsningen korrekt implementering av AbstractClass måste täcka alla egenskaper från Contract-gränssnittet? En enkel lösning är Impl implements Contract, men då måste jag duplicera deklarationer för alla egenskaper som inte har en komplex logikgetter. Så det skulle vara bra att också kunna använda den härledda typen av getBase()-implementationen. Målet är att få ett kompileringsfel om inget värde anges varken på Impl själv eller som en egenskap hos den härledda returtypen av getBase(). Är det i princip möjligt att göra med Typescript?

Svar #1

Om du gör getBase abstrakt och specificerar att returvärdet måste vara skillnaden mellan egenskaperna i den aktuella klassen och gränssnittet, kommer du att få ett kompileringstidfel om egenskapen inte finns i vare sig resultatet av getBase eller klassen:

abstract class AbstractClass<T, TThis> {
    abstract getBase(): { [P in Exclude<keyof T, keyof TThis>]: T[P] };
}
interface Contract {
    prop1: string;
    prop2: string;
}

class Impl extends AbstractClass<Contract, Impl> {
    get prop1() {
        // some complex logic here
        return '';
    }

    getBase() {
        return {
            prop2: ""
        }
    }
}

class ImplWrong  extends AbstractClass<Contract, ImplWrong> {
    get prop1() {
        // some complex logic here
        return '';
    }

    getBase() { // error Property 'getBase' in type 'ImplWrong' is not assignable to the same property in base type
        return {
            prop3: "" 
        }
    }
}

Du kommer att märka att jag var tvungen att skicka själva klassen som en typargument till basklassen, att använda this-typen är inte en lösning eftersom nycklarna i this aldrig är helt kända.

Också måste getBase returnera minst skillnaden mellan Impl och Contract, men det kan returnera fler egenskaper (typescript tillåter implementationer att returnera en supertyp av implementationsmetoden). Så om du har prop1 både i klassen och i returvärdet av getBase kommer det inte att vara ett fel.

Svar #2

En annan lösning är att lägga till en begränsning för Impl-klassen och låta getBase returtyp härledas:

abstract class AbstractClass<T> {
  abstract getData(): T;
  getBase(): Partial<T> {
    return {};
  }
}

interface Contract {
  prop1: string;
  prop2: string;
}


type ImplConstraint<T, I extends AbstractClass<T>> = 
  { [n in Exclude<keyof T, keyof ReturnType<I['getBase']>>]: T[n] };

class Impl extends AbstractClass<Contract> implements ImplConstraint<Contract, Impl>  {
  get prop1() {
    // some complex logic here
    return '';
  }

  getBase() {
    return {
      prop2: 'foo'
    }
  }

  getData(): Contract {
       return {} as Contract;    
  }  

}

class ImplWrong extends AbstractClass<Contract> implements ImplConstraint<Contract, ImplWrong>  {

// Class 'ImplWrong' incorrectly implements interface 
//  'ImplConstraint<Contract, Impl>'
//       Property 'prop1' is missing in type 'ImplWrong'.

  get prop11() {
    // some complex logic here
    return '';
  }

  getBase() {
    return {
      prop2: 'foo'
    }
  }

  getData(): Contract {
       return {} as Contract;    
  }  

}

Relaterade artiklar