TypeScript:提取字符串文字作为函数参数
TypeScript: Extract string literal as function param
我正在使用 meteor.js 和 TypeScript 并尝试制作强类型的流星方法。
为此,我创建了一个文件,其中包含我的方法的类型定义,如下所示:
export interface ClientWithSecret {
id: string;
secret: string;
}
export interface MeteorMethod {
name: string;
args: any[];
return: any;
}
export interface NewGameMethod extends MeteorMethod {
name: "newGame";
args: [auth: ClientWithSecret];
return: string;
}
export interface NewClientMethod extends MeteorMethod {
name: "newClient";
args: [];
return: ClientWithSecret;
}
export interface LoginMethod extends MeteorMethod {
name: "login";
args: [auth: ClientWithSecret];
return: true | ClientWithSecret;
}
export type ValidMethods = NewGameMethod | NewClientMethod | LoginMethod;
现在我正在尝试创建一个方法,将普通的流星方法(使用回调...)包装到一个返回承诺的函数中,如下所示:
export function meteorCallAsync<T extends MeteorMethod>(methodName: T["name"], args: T["args"]): Promise<T["return"]> {
return new Promise((resolve, reject) => {
Meteor.call(methodName, ...args, (error: Meteor.Error, result: T["return"]) => {
if (error) {
reject(error);
}
resolve(result);
});
});
}
这似乎很有效。我可以等待这样的流星方法
const retVal = await meteorCallAsync<NewGameMethod>("newGame", [getClientWithSecret()]);
并且 TypeScript 实际上检查字符串 "newGame"
是否等于定义为 NewGameMethod
名称的字符串文字。完美,但我有两个问题,因为我是 TypeScript 的新手:
可不可以把meteorCallAsync
的第一个参数一并省略,让TypeScript编译器补上?我已经将类型定义为泛型,所以编译器确实有必要的信息来填充它,但我不知道 TypeScript 是否支持它
有没有办法将 MeteorMethod
接口定义为某种无法实例化的抽象接口? ValidMethods
实际上是 meteorCallAsync<T extends ValidMethods>
更合适的类型吗?有没有办法让我强制每个方法实际上必须有 name
、args
和 return
?
编辑:
我在下面添加了 newGame
方法的实现。问题是我不知道如何告诉 TypeScript Meteor.call(name, ...args, (error, result)=>{})
实际上调用了 Meteor.methods
中定义的函数
Meteor.methods({
// create a new game
newGame(auth: ClientWithSecret) {
if (!isValidClient(auth)) {
console.error(`client invalid ${auth.id}`);
return;
}
let randomId,
newIdFound = false;
while (!newIdFound) {
randomId = Random.id();
const game = GamesCollection.findOne({ _id: randomId });
if (!game) {
newIdFound = true;
}
}
GamesCollection.insert({
_id: randomId,
hostId: auth.id,
clientIds: [auth.id],
players: [],
createdAt: new Date(Date.now()),
});
return randomId;
},
newClient(): ClientWithSecret {
//implementation
},
login(auth: ClientWithSecret): true | ClientWithSecret {
// returns true if login successful, new ClientWithSecret if credentials invalid
},
});
背景
您不必为每个函数键入接口,因为该信息已经存在于您的代码库中的某个地方。如果您知道它们是函数本身的类型,那么您可以使用 ReturnType and Parameters 从函数的类型中导出 args 和 return 的类型。我们在这里缺少的部分是函数名称和函数类型之间的关联。
我不熟悉 Meteor,所以我不得不查看文档以了解其工作原理。事实证明,这些类型的定义非常松散。
Meteor.call()
允许您传递带有任何参数的任何函数名称。
function call(name: string, ...args: any[]): any;
像您在这里做的那样围绕这个函数创建一个包装器是明智的。您将获得更好的类型安全和更好的自动完成支持。您可以使用声明合并来增加包类型,但包装它更容易实现。
可调用函数名称通过使用方法字典对象调用 Meteor.methods()
来定义。
function methods(methods: {[key: string]: (this: MethodThisType, ...args: any[]) => any}): void;
解决方案
我们想获取您的特定词典的类型。我们将使用一个中间变量而不是在 Meteor.methods()
中定义方法,这样我们就可以在该变量上使用 typeof
来获取您的字典类型。
// define the methods
const myMethods = {
newGame(auth: ClientWithSecret) {
....
}
// set the methods on Meteor
Meteor.methods(myMethods);
// get the type
type MyMeteorMethods = typeof myMethods;
然后我们使用 MyMeteorMethods
类型来注释您的 meteorCallAsync
函数。
export function meteorCallAsync<T extends keyof MyMeteorMethods>(
methodName: T,
...args: Parameters<MyMeteorMethods[T]>
): Promise<ReturnType<MyMeteorMethods[T]>> {
return new Promise((resolve, reject) => {
Meteor.call(methodName, ...args, (error: Meteor.Error, result: ReturnType<MyMeteorMethods[T]>) => {
if (error) {
reject(error);
}
resolve(result);
});
});
}
T
是方法名,必须是你字典里的关键字。
MyMeteorMethods[T]
是方法的类型。
- 您的
args
与方法的参数匹配。我将 args
更改为 ...args
以便您可以单独传递参数而不是在数组中。
- 您的 return 类型是方法 return 类型的
Promise
。
现在调用函数时不需要设置任何类型。 Typescript 可以根据 methodName
推断出正确的类型。您会在需要的地方出现错误,在不需要的地方不会出现错误。
const x = async () => {
// ok to call with the correct arguments
const retVal1 = await meteorCallAsync("newGame", getClientWithSecret());
// error if required arguments are missing
// 'Arguments for the rest parameter 'args' were not provided.'
const retVal2 = await meteorCallAsync("newGame");
// ok to call with no arguments if the method doesn't require any
const retVal3 = await meteorCallAsync("newClient");
// error if calling an invalid method name
// 'Argument of type '"invalidFunc"' is not assignable to parameter of type '"newGame" | "newClient" | "login"''
const retVal4 = await meteorCallAsync("invalidFunc");
}
高级
如果您想在方法对象的任何方法中使用 this
,则需要一些技巧。我们想要在此处使用的一些类型(例如 MethodThisType
)未导出,因此我们需要向后工作以获取它们。
type MeteorMethodDict = Parameters<typeof Meteor.methods>[0]
这为我们提供了该方法字典对象的类型,其中每个条目都是一个 this
类型为 MethodThisType
的函数。
我们希望确保您的方法扩展了 MeteorMethodDict
类型,而不会扩大类型并丢失有关您的特定方法的信息。所以我们可以通过一个强制类型的身份函数来创建方法。
const makeMethods = <T extends MeteorMethodDict>(methods: T): T => methods;
现在您可以在任何方法中使用 this
,它将具有正确的类型。
const myMethods = makeMethods({
newGame(auth: ClientWithSecret) {
const userId = this.userId;
...
我们从 type MyMeteorMethods = typeof myMethods
得到的类型将包含 this
类型,无论您是否使用它。
type MyMeteorMethods = {
newGame(this: Meteor.MethodThisType, auth: ClientWithSecret): any;
newClient(this: Meteor.MethodThisType): ClientWithSecret;
login(this: Meteor.MethodThisType, auth: ClientWithSecret): true | ClientWithSecret;
}
我正在使用 meteor.js 和 TypeScript 并尝试制作强类型的流星方法。
为此,我创建了一个文件,其中包含我的方法的类型定义,如下所示:
export interface ClientWithSecret {
id: string;
secret: string;
}
export interface MeteorMethod {
name: string;
args: any[];
return: any;
}
export interface NewGameMethod extends MeteorMethod {
name: "newGame";
args: [auth: ClientWithSecret];
return: string;
}
export interface NewClientMethod extends MeteorMethod {
name: "newClient";
args: [];
return: ClientWithSecret;
}
export interface LoginMethod extends MeteorMethod {
name: "login";
args: [auth: ClientWithSecret];
return: true | ClientWithSecret;
}
export type ValidMethods = NewGameMethod | NewClientMethod | LoginMethod;
现在我正在尝试创建一个方法,将普通的流星方法(使用回调...)包装到一个返回承诺的函数中,如下所示:
export function meteorCallAsync<T extends MeteorMethod>(methodName: T["name"], args: T["args"]): Promise<T["return"]> {
return new Promise((resolve, reject) => {
Meteor.call(methodName, ...args, (error: Meteor.Error, result: T["return"]) => {
if (error) {
reject(error);
}
resolve(result);
});
});
}
这似乎很有效。我可以等待这样的流星方法
const retVal = await meteorCallAsync<NewGameMethod>("newGame", [getClientWithSecret()]);
并且 TypeScript 实际上检查字符串 "newGame"
是否等于定义为 NewGameMethod
名称的字符串文字。完美,但我有两个问题,因为我是 TypeScript 的新手:
可不可以把
meteorCallAsync
的第一个参数一并省略,让TypeScript编译器补上?我已经将类型定义为泛型,所以编译器确实有必要的信息来填充它,但我不知道 TypeScript 是否支持它有没有办法将
MeteorMethod
接口定义为某种无法实例化的抽象接口?ValidMethods
实际上是meteorCallAsync<T extends ValidMethods>
更合适的类型吗?有没有办法让我强制每个方法实际上必须有name
、args
和return
?
编辑:
我在下面添加了 newGame
方法的实现。问题是我不知道如何告诉 TypeScript Meteor.call(name, ...args, (error, result)=>{})
实际上调用了 Meteor.methods
Meteor.methods({
// create a new game
newGame(auth: ClientWithSecret) {
if (!isValidClient(auth)) {
console.error(`client invalid ${auth.id}`);
return;
}
let randomId,
newIdFound = false;
while (!newIdFound) {
randomId = Random.id();
const game = GamesCollection.findOne({ _id: randomId });
if (!game) {
newIdFound = true;
}
}
GamesCollection.insert({
_id: randomId,
hostId: auth.id,
clientIds: [auth.id],
players: [],
createdAt: new Date(Date.now()),
});
return randomId;
},
newClient(): ClientWithSecret {
//implementation
},
login(auth: ClientWithSecret): true | ClientWithSecret {
// returns true if login successful, new ClientWithSecret if credentials invalid
},
});
背景
您不必为每个函数键入接口,因为该信息已经存在于您的代码库中的某个地方。如果您知道它们是函数本身的类型,那么您可以使用 ReturnType and Parameters 从函数的类型中导出 args 和 return 的类型。我们在这里缺少的部分是函数名称和函数类型之间的关联。
我不熟悉 Meteor,所以我不得不查看文档以了解其工作原理。事实证明,这些类型的定义非常松散。
Meteor.call()
允许您传递带有任何参数的任何函数名称。
function call(name: string, ...args: any[]): any;
像您在这里做的那样围绕这个函数创建一个包装器是明智的。您将获得更好的类型安全和更好的自动完成支持。您可以使用声明合并来增加包类型,但包装它更容易实现。
可调用函数名称通过使用方法字典对象调用 Meteor.methods()
来定义。
function methods(methods: {[key: string]: (this: MethodThisType, ...args: any[]) => any}): void;
解决方案
我们想获取您的特定词典的类型。我们将使用一个中间变量而不是在 Meteor.methods()
中定义方法,这样我们就可以在该变量上使用 typeof
来获取您的字典类型。
// define the methods
const myMethods = {
newGame(auth: ClientWithSecret) {
....
}
// set the methods on Meteor
Meteor.methods(myMethods);
// get the type
type MyMeteorMethods = typeof myMethods;
然后我们使用 MyMeteorMethods
类型来注释您的 meteorCallAsync
函数。
export function meteorCallAsync<T extends keyof MyMeteorMethods>(
methodName: T,
...args: Parameters<MyMeteorMethods[T]>
): Promise<ReturnType<MyMeteorMethods[T]>> {
return new Promise((resolve, reject) => {
Meteor.call(methodName, ...args, (error: Meteor.Error, result: ReturnType<MyMeteorMethods[T]>) => {
if (error) {
reject(error);
}
resolve(result);
});
});
}
T
是方法名,必须是你字典里的关键字。MyMeteorMethods[T]
是方法的类型。- 您的
args
与方法的参数匹配。我将args
更改为...args
以便您可以单独传递参数而不是在数组中。 - 您的 return 类型是方法 return 类型的
Promise
。
现在调用函数时不需要设置任何类型。 Typescript 可以根据 methodName
推断出正确的类型。您会在需要的地方出现错误,在不需要的地方不会出现错误。
const x = async () => {
// ok to call with the correct arguments
const retVal1 = await meteorCallAsync("newGame", getClientWithSecret());
// error if required arguments are missing
// 'Arguments for the rest parameter 'args' were not provided.'
const retVal2 = await meteorCallAsync("newGame");
// ok to call with no arguments if the method doesn't require any
const retVal3 = await meteorCallAsync("newClient");
// error if calling an invalid method name
// 'Argument of type '"invalidFunc"' is not assignable to parameter of type '"newGame" | "newClient" | "login"''
const retVal4 = await meteorCallAsync("invalidFunc");
}
高级
如果您想在方法对象的任何方法中使用 this
,则需要一些技巧。我们想要在此处使用的一些类型(例如 MethodThisType
)未导出,因此我们需要向后工作以获取它们。
type MeteorMethodDict = Parameters<typeof Meteor.methods>[0]
这为我们提供了该方法字典对象的类型,其中每个条目都是一个 this
类型为 MethodThisType
的函数。
我们希望确保您的方法扩展了 MeteorMethodDict
类型,而不会扩大类型并丢失有关您的特定方法的信息。所以我们可以通过一个强制类型的身份函数来创建方法。
const makeMethods = <T extends MeteorMethodDict>(methods: T): T => methods;
现在您可以在任何方法中使用 this
,它将具有正确的类型。
const myMethods = makeMethods({
newGame(auth: ClientWithSecret) {
const userId = this.userId;
...
我们从 type MyMeteorMethods = typeof myMethods
得到的类型将包含 this
类型,无论您是否使用它。
type MyMeteorMethods = {
newGame(this: Meteor.MethodThisType, auth: ClientWithSecret): any;
newClient(this: Meteor.MethodThisType): ClientWithSecret;
login(this: Meteor.MethodThisType, auth: ClientWithSecret): true | ClientWithSecret;
}