简体   繁体   English

TypeScript 中有没有办法从扩展固定接口的泛型中获取类型?

[英]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.我有一个 class 有一些方法可以进行特定的 api 调用。 This class get, as constructor parameter, an interface where there are a sets of methods used to transform the API responses in something else.这个 class 作为构造函数参数获取一个接口,其中有一组方法用于将 API 响应转换为其他内容。 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.但是从API class来看,是不需要知道transformer实现的,这种方式可以用任何方式实现transformer。 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:我已经尝试过使用 generics 并且我最好的猜测是这样的:

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" .给定一个扩展IATransformer的泛型类型T ,类型TtransformResOne()方法的返回类型可以写为ReturnType<T['transformResOne']> ,使用ReturnType实用程序类型获取返回类型,而T[K] 索引访问运算符以获取键"transformResOne"处的T成员的类型。 And an analogous statement can be made for transformResTwo .并且可以对transformResTwo做出类似的声明。 That means we can defined IA and A like this:这意味着我们可以这样定义IAA

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 .但是,请注意,在A的实现中,编译器不能真正遵循这种涉及条件类型(如ReturnType )的高阶类型杂耍,该条件类型依赖于未解析的泛型类型参数(如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 .相反,它需要一些捷径,例如推断this.transformer.transformResOne(res)返回与IATransformertransformResOne(res)方法所做的相同的事情,即any type The any type is intentionally loose, and so the compiler will not notice or warn you if you use it incorrectly: any类型是故意松散的,因此如果您错误地使用它,编译器不会注意到或警告您:

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 :一种仍然允许良好类型推断的方法是重构以使所有返回类型T1T2中的所有内容都通用:

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() ).正如您所提到的,您可能不想对具有大量类型参数的东西执行此操作,但是您可以看到编译器在捕获错误和推断类型方面要好得多(您甚至不必注释返回类型getOne()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 Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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