简体   繁体   中英

Is there a way in TypeScript to get typing from a generic that extend a fixed interface?

The problem is:

I've a class that have some methods that make specific api calls. This class get, as constructor parameter, an interface where there are a sets of methods used to transform the API responses in something else. But from the API class prospective, is not necessary to know the trasformer implementation, in this way is possible to implement the transformer in any way. What I would like to do is to know the returned type once everything has been instantiated.

Code example of the wanted behaviour


type SomeObj = {
  stringKey: string;
  numberKey: number;
}

type SomeOtherObj = {
  stringKey: string;
  numberKey: number;
}

function fetch1() {
  return { stringKey: 'stringKey', numberKey: 2 }
}

function fetch2() {
  return { stringKey: 'stringKey', numberKey: 2 }
}

interface IATransformer {
  transformResOne: (res1: SomeObj) => any;
  transformResTwo: (res2: SomeOtherObj) => any;
}

class ATransformer {
  transformResOne(res1: SomeObj): string {
    return res1.stringKey
  }
  transformResTwo(res2: SomeOtherObj): number {
    return res2.numberKey;
  }
}

interface IA<T extends ATransformer> {
  getOne: (param: string) => any; // Not "any" but something from the generic
  getTwo: (param2: string) => any; // Not "any" but something from the generic
}

class A<T extends ATransformer> implements IA<T> {
  constructor(
    private readonly transformer: T
  ) { }

  // Return type doesn't work in this way but don't know how to sobstitute it
  getOne(param: string): ReturnType<this.transformer.getOne> {
    const res = fetch1()
    return this.transformer.transformResOne(res)
  }

  // Return type doesn't work in this way but don't know how to sobstitute it
  getTwo(param2: string): ReturnType<this.transformer.getTwo> {
    const res = fetch2();
    return this.transformer.transformResTwo(res)
  }
}

const transformer = new ATransformer();
// Generic is inferred from the parameter
const a = new A(transformer);

// Desired result ---> ERROR: the type returned from the function is not number but string
const resOneResult: number = a.getOne('Some string')

I already tried by using generics and my best guess was something like this:

class A implements IA {
  constructor(
    private readonly transformer: IATransformer
  ) { }

  getOne(param: string): ReturnType<typeof this.transformer.transformResOne> {
    const res = fetch1()
    return this.transformer.transformResOne(res)
  }

  getTwo(param2: string): ReturnType<typeof this.transformer.transformResTwo> {
    const res = fetch2();
    return this.transformer.transformResTwo(res)
  }
}

But of course it doesn't work!

Given a generic type T that extends IATransformer , the return type of the transformResOne() method of type T can be written as ReturnType<T['transformResOne']> , using the ReturnType utility type to get the return type, and the T[K] indexed access operator to get the type of the member of T at key "transformResOne" . And an analogous statement can be made for transformResTwo . That means we can defined IA and A like this:

interface IA<T extends IATransformer> {
  getOne: (param: string) => ReturnType<T['transformResOne']>;
  getTwo: (param2: string) => ReturnType<T['transformResTwo']>;
}

class A<T extends IATransformer> implements IA<T> {
  constructor(
    private readonly transformer: T
  ) { }

  getOne(param: string): ReturnType<T['transformResOne']> {
    const res = fetch1()
    return this.transformer.transformResOne(res)
  }

  getTwo(param2: string): ReturnType<T['transformResTwo']> {
    const res = fetch2();
    return this.transformer.transformResTwo(res)
  }
}

This should work as you want:

const transformer = new ATransformer();
const a = new A(transformer);
const resOneResult: number = a.getOne('Some string'); // error!
// Type 'string' is not assignable to type 'number'

However, note that inside the implementation of A , the compiler can't really follow such higher-order type juggling involving a conditional type like ReturnType that depends on an unresolved generic type parameter like T .

Instead it takes some shortcuts, like inferring that this.transformer.transformResOne(res) returns the same thing that IATransformer 's transformResOne(res) method does, namely the any type . The any type is intentionally loose, and so the compiler will not notice or warn you if you use it incorrectly:

getOops(param2: string): ReturnType<T['transformResTwo']> {
  const res = fetch2();
  return this.transformer.transformResOne(res); // no error, inferred as any
}

So you have to be careful with such an implementation.


If you want to ensure more type safety, I'd recommend avoiding such conditional types, if possible. One way to do this which still allows good type inference is to refactor to make everything generic in those return types T1 and T2 :

interface IA<T1, T2> {
  getOne: (param: string) => T1;
  getTwo: (param2: string) => T2;
}

interface IATransformer<T1, T2> {
  transformResOne: (res1: SomeObj) => T1;
  transformResTwo: (res2: SomeOtherObj) => T2;
}

class A<T1, T2> implements IA<T1, T2> {
  constructor(
    private readonly transformer: IATransformer<T1, T2>
  ) { }

  getOne(param: string) {
    const res = fetch1()
    return this.transformer.transformResOne(res)
  }

  getTwo(param2: string) {
    const res = fetch2();
    return this.transformer.transformResTwo(res)
  }

  getOops(param2: string): T2 {
    const res = fetch2();
    return this.transformer.transformResOne(res); // error!
  }
}

As you mentioned, you might not want to do this for something with a large number of type parameters, but you can see that the compiler is much better about catching errors and inferring types (you don't even have to annotate the return types of getOne() and getTwo() ). Let's just be sure that it still works though:

const transformer = new ATransformer();
const a = new A(transformer);
const resOneResult: number = a.getOne('Some string'); // error!
// Type 'string' is not assignable to type 'number'

Looks good!

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM