Typescript泛型调用对象的方法

xxe27gdn  于 2023-10-22  发布在  TypeScript
关注(0)|答案(2)|浏览(144)

我有这样一个类(“简化”):

class MyDb {
    create(user: {name: string, id: number}) {
        // 
    }
    delete(id: number) {
        // 
    }
    read(id: number) {
        // 
    }
}
const db = new MyDb()

我得到了这样一个函数:

function processRequest <Method extends keyof MyDb, Data extends Parameters<MyDb[Method]>> (
    method: Method,
    data: Data

) {
    db[method](data)  // here goes error. Argument of type 'number | { name: string; id: number; }' is not assignable to parameter of type '{ name: string; id: number; } & number'.
}

我试过使用Parameters<...>[0]db[method](...data),但它对我没有帮助。另外,对我来说有趣的是,我对泛型的想法是,当我调用那个函数时,“类型”会被推断出来,但在这种情况下,它在编写过程中不知何故会中断。
操场

jm2pwxwz

jm2pwxwz1#

TypeScript不能像processRequest()主体中的Parameters<MyDb[K]>那样真正对genericconditional types执行抽象推理(因为KMethod是泛型类型参数,而Parameters实用程序是用条件类型实现的)。这样的类型对于类型检查器来说基本上是不透明的,因此为了评估它,db[method]的类型从泛型MyDb[K]扩展到函数MyDb[keyof MyDb]的并集。通常,你不能安全地调用一个函数的联合,除非你传递给它一个参数的交集(参见发行说明,了解对调用函数联合的支持)。基本上,db[method]类型和data类型之间的通用相关性已经丢失。
如果你不关心编译器理解相关性的能力,你可以只使用类型Assert,然后继续你的一天:

(db[method] as (arg: typeof data) => void)(data); // okay

但是如果你想让编译器遵循你的推理,那么推荐的方法,如microsoft/TypeScript#47109中所描述的,是重构你的类型操作,这样它们就可以用一些“基本”Map类型来表示,泛型indexed accessesMap到该基本类型,mapped typesMap到该基本类型。这将最终使用与您所拥有的类型等效的类型,但它们将以一种让编译器“看到”预期关系的方式表示。
这里有一个办法。首先,让我们将基类型定义为Db方法的参数。这可以通过编程实现:

type DbParams = { [K in keyof MyDb]: Parameters<MyDb[K]>[0] }        

/* type DbParams = {
    create: {
        name: string;
        id: number;
    };
    delete: number;
    read: number;
} */

然后可以写processRequest,使得data直接是DbParams[K]

function processRequest<K extends keyof MyDb>(
    method: K,
    data: DbParams[K]
) {
    const _db: { [P in keyof MyDb]: (arg: DbParams[P]) => void } = db;
    _db[method](data); // okay
}

重要的是,我们已经将Db类型的db赋值给了Map类型为{ [P in keyof MyDb]: (arg: DbParams[P]) => void }的变量_db。这相当于Db,但它被显式地写为基础DbParams类型的Map类型。赋值成功是因为它实际上不是泛型(Db和Map类型都是不依赖于K的特定类型)。
现在通话成功了_db[method]的类型被显式地视为(arg: DbParams[K]) => void。这是一个接受DbParams[P]类型参数的单个调用签名。由于这正是data的类型,因此调用_db[method](data)成功。
同样,此版本与您的版本之间的唯一区别是类型的表示形式,如microsoft/TypeScript#47109中所述的泛型/Map/索引版本。如果这不是必须的,那就太好了,但是现在,如果你想避免类型Assert或类似的东西,这是你必须做的。
Playground链接到代码

fumotvh3

fumotvh32#

class MyDb {
    create(user: { name: string, id: number }) {
        // 
    }
    delete(id: number) {
        // 
    }
    read(id: number) {
        // 
    }
}

const db = new MyDb();

type MyDbKeys = keyof MyDb

// Conditional type that determines the type of the first argument of a method
type MethodArgs<T, M extends keyof T> = T[M] extends (...args: infer Parameters) => unknown ? Parameters[0] : never;

// Intermediate function for type safety
function callDbMethod<Method extends MyDbKeys >(method: Method, data: MethodArgs<MyDb, Method>) {
    return (db[method] as (...args: [MethodArgs<MyDb, Method>]) => void)(data);
}

function processRequest<Method extends MyDbKeys >(
    method: Method,
    data: MethodArgs<MyDb, Method>
) {
    callDbMethod(method, data);
}

// Usage
processRequest('create', { name: 'John', id: 1 });  // works
processRequest('delete', 1);                       // works
processRequest('read', 2);                         // works

我只添加了callDbMethod,用于安全地调用方法。您的实现可能已经获得了类型化参数。技巧是将as与**infinite类型的

相关问题