ilj
ilj

Reputation: 869

Typescript generic constraint that uses inferred method return type

i have this 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'
    }
  }
}

how do i express the constraint correct implementation of AbstractClass has to cover all properties from Contract interface? simple solution is Impl implements Contract, but then i will have to duplicate declarations for all properties that do not have a complex logic getter. so it would be nice to be able to also use inferred type of getBase() implementation. the goal is to have a compile-time error if there is no value provided either on Impl itself or as property of inferred return type of getBase(). is it possible in principle using Typescript?

Upvotes: 3

Views: 403

Answers (2)

artem
artem

Reputation: 51629

Another solution is to add constraint for Impl class, and leave getBase return type inferred:

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;    
  }  

}

Upvotes: 2

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249676

If you make getBase abstract and specify that the return must be the difference of the properties of the current class and the interface, you will get a compile time error if the property is not in either the result of getBase or the class:

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: "" 
        }
    }
}

You will notice that I had to pass the class itself as a type argumentto the base class, using the this type is not a solution as the keys of this are never fully known.

Also getBase must return at the very least the difference between Impl and Contract but it could return more properties (typescript allows implementations to return a super type of the implementation method). So if you have prop1 in both the class and the return of getBase it will not be an error.

Upvotes: 2

Related Questions