简体   繁体   English

如何让 TypeScript 推断实现通用接口的 object 的特定类型?

[英]How to let TypeScript infer the specific types of an object implementing a generic interface?

In this code example, TypeScript does not seem to know about the exact type of userService.getByUserId in the very last line, which should be (userId: string) => ServiceResult<User> .在此代码示例中,TypeScript 似乎不知道最后一行中userService.getByUserId的确切类型,应该是(userId: string) => ServiceResult<User> Instead, it insists on its more generic type (...args: Array<any>) => ServiceResult<User> , defined in the Service interface.相反,它坚持使用在Service接口中定义的更通用的类型(...args: Array<any>) => ServiceResult<User>

How can I make sure that both any object that implements a Service adheres to the interface definition and TypeScript knows about the actual type definitions of any implemented Service , eg userService.getByUserId in this example?如何确保实现Service的任何 object 都遵守接口定义,并且TypeScript 知道任何已实现Service实际类型定义,例如本例中的userService.getByUserId

Thanks!谢谢!

type ServiceResult<T> = T | Promise<T> | undefined;

/**
 * When implementing a `Service`, there must be a `get` function that takes a
 * single argument `id` of type `string` and returns a `ServiceResult`.
 * 
 * Any other property on an object that implements `Service` must be a function
 * that takes any number of arbitrary arguments and returns a `ServiceResult`.
 */
interface Service<T> {
  get: (id: string) => ServiceResult<T>;
  [key: string]: (...args: Array<any>) => ServiceResult<T>;
}

type User =  {
  id: string;
  name?: string;
};

const users: Array<User> = [
  {
    id: 'a',
    name: 'Jon',
  },
];


const userService: Service<User> = {
  get: (id: string) =>  users.find((user) => user.id === id),
  /**
   * Here, `getByUserId` has a well defined type of
   * `(userId: string) => ServiceResult<User>`
   */
  getByUserId: (userId: string) =>  users.find((user) => user.id === userId),
};

/**
 * However, when invoking `getByUserId`, TypeScript does not seem to know about
 * its exact type definition and instead insists on
 * `(...args: any[]) => ServiceResult<User>`
 */
userService.getByUserId();

Since userService has no knowledge about the actual type of getByUserId , you have to overload it explicitly,由于userService不知道getByUserId的实际类型,因此您必须显式地重载它,

const userService: Service<User> & {getByUserId: (userId: string) => ServiceResult<User>} = {
  get: (id: string) =>  users.find((user) => user.id === id),
  getByUserId: (userId: string) =>  users.find((user) => user.id === userId),
};
userService.getByUserId(); // error now

When you annotate a variable declaration with a (non-union) type, the compiler will forget about the type of any narrower expression that you assign to it.当您使用(非联合)类型注释变量声明时,编译器将忘记您分配给它的任何更窄表达式的类型。 There is no built-in functionality to say "please infer the variable's type but make sure it is assignable to this given type".没有内置功能可以说“请推断变量的类型,但确保它可以分配给这个给定的类型”。 You either leave off the annotation and let the compiler infer the type of the variable, or you annotate and get the exact type you annotated.您要么放弃注释并让编译器推断变量的类型,要么注释并获得您注释的确切类型。

There is an existing GitHub issue, microsoft/TypeScript#25214 , asking for such a feature, but I don't think that much is going to happen there.有一个现有的 GitHub 问题microsoft/TypeScript#25214要求提供这样的功能,但我认为那里不会发生太多事情。


One way to proceed is to just not annotate the variable, and if your initializer is incorrect, you'll get an error when you try to use it somewhere later.一种方法是不对变量进行注释,如果您的初始化程序不正确,您稍后尝试在某处使用它时会出现错误。 For example:例如:

const userServiceNoAnnotation = {
    get: (id: string) => users.find((user) => user.id === id),
    getByUserId: (userId: string) => users.find((user) => user.id === userId),
};

const badUserServiceNoAnnotation = {
    get: (id: number) => users.find((user) => user.id === id.toString()),
    getByUserId: (userId: string) => users.find((user) => user.id === userId),
};

Here I've left off the annotation, and the compiler infers the type of each variable as something narrow enough that it won't forget the parameters of getByUserId() .在这里,我省略了注释,编译器将每个变量的类型推断为足够窄,以至于它不会忘记getByUserId()的参数。 There's no error in badUserServiceNoAnnotation , yet . badUserServiceNoAnnotation没有错误。 But when we actually try to use them as a Service<User> , we get the behavior you want:但是当我们实际尝试将它们用作Service<User>时,我们会得到您想要的行为:

function expectUserService(us: Service<User>) { }
expectUserService(userServiceNoAnnotation);
expectUserService(badUserServiceNoAnnotation); // error!
// -------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~
//   Types of property 'get' are incompatible.

This might be good enough for your purposes.这对于您的目的可能已经足够了。


If you want an error closer to the declaration of your variable, you can abstract the "don't-annotate-and-try-to-use-it" to a helper function that uses it right away:如果您想要更接近变量声明的错误,您可以将“不要注释并尝试使用它”抽象为立即使用它的帮助程序 function:

const checkType = <T,>() => <U extends T>(u: U) => u;

const checkUserService = checkType<Service<User>>();

Here, checkType<T>() lets you specify what type T you'd like to check for, and returns an identity function that checks its input against type T without widening it.在这里, checkType<T>()允许您指定要检查的T类型,并返回一个标识 function ,该标识将其输入与类型T进行检查而不扩大它。 So checkUserService is such an identity function for Service<User> , and we can now use that instead of annotating.所以checkUserService就是这样一个身份 function 用于Service<User> ,我们现在可以使用它来代替注释。 Bad Service<User> s get the errors you expect: Bad Service<User>会得到您期望的错误:

const badUserService = checkUserService({
    get: (id: number) => users.find((user) => user.id === id.toString()), // error!
//  ~~~ <-- id param is incompatible    
    getByUserId: (userId: string) => users.find((user) => user.id === userId),
});

While good ones stay narrow enough for your purposes:虽然好的为您的目的保持足够窄:

const userService = checkUserService({
    get: (id: string) => users.find((user) => user.id === id),
    getByUserId: (userId: string) => users.find((user) => user.id === userId),
});

userService.getByUserId(); // error!
// -------> ~~~~~~~~~~~~~
// Expected 1 arguments, but got 0.

Playground link to code Playground 代码链接

You just need to declare your UserService as a class that implements your interface.您只需要将您的UserService声明为实现您的接口的 class。 Like so, you'll be able to narrow down certain methods to their correct types - and so you can tell at compile time what arguments a function requires.像这样,您将能够将某些方法缩小到正确的类型 - 因此您可以在编译时知道 arguments 和 function 需要什么。

type ServiceResult<T> = T | Promise<T> | undefined;

interface Service<T> {
    get: (id: string) => ServiceResult<T>;
    [key: string]: (...args: Array<any>) => ServiceResult<T>;
}

interface User {
    id: string;
    name: string;
}
const users: User[] =[
    {
        id: "a",
        name: "Jon"
    }
];

class UserService implements Service<User> {
    // This is required because Typescript complains if we don't add this line
    // EVERY property of this class must be this type of function
    [key: string]: (...args: Array<any>) => ServiceResult<User>;

    get (id: string) {
        return users.find(u => u.id === id);
    }

    getByName(name: string) {
        return users.find(u => u.name === name);
    }

    async getByIdAsync(id: string) {
        return new Promise<User>((resolve,reject) => {
            const user = this.get(id);
            if(user === undefined) {
                return reject();
            }
            return resolve(user);
        });
    }
}

// Create the class however you like. Dependency Injection, just call `new`, w/e
declare const userService: UserService;

// result is User|undefined
const result = userService.getByName('');
// asyncResult is Promise<User>
const asyncResult = userService.getByIdAsync('');

When you declare an object as Service<T> , TS can't predict what will happen to that object, for example:当您将 object 声明为Service<T>时,TS 无法预测该 object 会发生什么,例如:

const userService: Service<User> = {
  get: (id: string) =>  users.find((user) => user.id === id),
  /**
   * Here, `getByUserId` has a well defined type of
   * `(userId: string) => ServiceResult<User>`
   */
  getByUserId: (userId: string) =>  users.find((user) => user.id === userId),
};
// this is perfectly legal
userService.getByUserId = (userId:string, somethingElse:number) => users.find((user) => user.id === userId);
// woops! getByUserId now has a different type, even though it conforms to Service<User>

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

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