繁体   English   中英

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

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

在此代码示例中,TypeScript 似乎不知道最后一行中userService.getByUserId的确切类型,应该是(userId: string) => ServiceResult<User> 相反,它坚持使用在Service接口中定义的更通用的类型(...args: Array<any>) => ServiceResult<User>

如何确保实现Service的任何 object 都遵守接口定义,并且TypeScript 知道任何已实现Service实际类型定义,例如本例中的userService.getByUserId

谢谢!

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();

由于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

当您使用(非联合)类型注释变量声明时,编译器将忘记您分配给它的任何更窄表达式的类型。 没有内置功能可以说“请推断变量的类型,但确保它可以分配给这个给定的类型”。 您要么放弃注释并让编译器推断变量的类型,要么注释并获得您注释的确切类型。

有一个现有的 GitHub 问题microsoft/TypeScript#25214要求提供这样的功能,但我认为那里不会发生太多事情。


一种方法是不对变量进行注释,如果您的初始化程序不正确,您稍后尝试在某处使用它时会出现错误。 例如:

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

在这里,我省略了注释,编译器将每个变量的类型推断为足够窄,以至于它不会忘记getByUserId()的参数。 badUserServiceNoAnnotation没有错误。 但是当我们实际尝试将它们用作Service<User>时,我们会得到您想要的行为:

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

这对于您的目的可能已经足够了。


如果您想要更接近变量声明的错误,您可以将“不要注释并尝试使用它”抽象为立即使用它的帮助程序 function:

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

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

在这里, checkType<T>()允许您指定要检查的T类型,并返回一个标识 function ,该标识将其输入与类型T进行检查而不扩大它。 所以checkUserService就是这样一个身份 function 用于Service<User> ,我们现在可以使用它来代替注释。 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),
});

虽然好的为您的目的保持足够窄:

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 代码链接

您只需要将您的UserService声明为实现您的接口的 class。 像这样,您将能够将某些方法缩小到正确的类型 - 因此您可以在编译时知道 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('');

当您将 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