![](/img/trans.png)
[英]Let typescript infer the type of of a class method, implementing an interface
[英]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.
您只需要将您的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.