如何在 TypeScript 中正确键入和转换泛型?

How to correctly type and cast a generic in TypeScript?

我不知道如何正确地问这个问题,因为我不明白打字在 TypeScript 中是如何与泛型一起工作的,但我知道如何解释它,例如,假设我有一个“函数存储” :

const functionStore: Array <Function> = [];

我有一个函数可以将函数添加到函数存储中,这个函数允许接收一个对象并指定将添加到由参数传递的对象的函数的键的名称:

const addFunction = <Obj = { [index: string]: any }>(
    obj: Obj,
    ...extractFunction: Array<keyof Obj>
) => {
    for (const key of extractFunction) {
        functionStore.push(obj[key]);
    }
};

因此,如果我创建以下对象并将其传递给 addFunction

const obj = {
    msg: 'Welcome!',
    test() {
        console.log('Hello!!');
    },
    doNoAdd() {
        console.log('Do not add!');
    },
    otherTest() {
        console.log('Other test!!');
    }
};

addFunction(obj, 'test', 'otherTest');

这不起作用,因为“'Obj[keyof Obj]' 类型的参数不能分配给'Function' 类型的参数”:

...
for (const key of extractFunction) {
    functionStore.push(obj[key]); //<-- Error!!
}
...

如果我进行转换,它仍然会报错,因为是通用的,属性 可以是 stringnumberarray等(而且我认为可能有更好的选择,而不是首先转换为 unknownany):

...
functionStore.push(obj[key] as Function); //<-- Conversion of type 'Obj[keyof Obj]' to type 
                                          //     'Function' may be a mistake because neither
                                          //     type sufficiently overlaps with the other. 
                                          //     If this was intentional, convert the
                                          //      expression to 'unknown' first.
...

我如何进行键入,其中指定这些键,除了是 obj 的键外,我还可以指定它指的是对象内部的函数 obj?

希望你能帮助我:)

问题在于定义泛型的方式,= 运算符分配一个默认类型,这可能永远不会发生,因为它总是由 TS 从类型 Obj。在这里修复它的方法是使用 extend 运算符,它将限制参数以扩展提供的接口。

像这样:

// this is pretty cool
const functionStore: Array <Function> = [];

// so it should extend the interface that you have defined
const addFunction = <Obj extends { [index: string]: any }>(
    obj: Obj,
    ...extractFunction: Array<keyof Obj>
) => {
    for (const key of extractFunction) {
        functionStore.push(obj[key]);
    }
};

应该这样做 - playground


补充一下,如果您查看 playground link 并将鼠标悬停在 addFunction 调用上,您会注意到泛型类型是由您提供的参数推断出来的。这是关于 Generic Constraints.

的文档

为了更安全一点,可以在Obj.

中添加更多的约束条件
type Fn = (...args: any[]) => any

const functionStore: Array<Fn> = [];


type ExtractFn<T> = {
    [Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]

const addFunction = <Obj extends Record<string, any>>(
    obj: Obj,
    ...extractFunction: Array<ExtractFn<Obj>>
) => {
    for (const key of extractFunction) {
        functionStore.push(obj[key]);
    }
};
const obj = { name: 'John', getName: () => 'John' }

addFunction(obj, 'getName') // ok
addFunction(obj, 'name') // expected error

Playground 您可能已经注意到,您不能提供与功能不对应的密钥。

解释

[Prop in keyof T] - 参见 mapped types。它遍历 T 类型中的每个 属性。就像 javascript.

中的 for..in 循环

T[Prop] - 参见 indexed accesstype A={age:42}; type B = A['age'] // 42。与 javascript.

中的工作方式相同

T[Prop] extends Fn ? Prop : never - 参见 conditional types。表示如果T[Prop]Fn的子类型->returnProp,否则-never.

因此:

type ExtractFn<T> = {
    [Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}

遍历类型并检查 属性 是否为函数。如果它是一个函数,将此函数替换为 属性 名称,否则 - 替换为 never.

最后一件事,使用 [keyof T]:

type ExtractFn<T> = {
    [Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]

没有keyof T,ExtractFnreturns:

ExtractFn<{msg:'string', doThing:()=>any}> // {msg:never, doThing:'doThing'}

所以我们需要获得所有值的并集。使用 keyof T 很容易做到。事实上,它 returns never | 'doThing'。因为 never 可以分配给任何类型,所以 never | 'doThing' 与 'doThing' 冲突。

示例:

type Foo={age:42,name:string}['age'|'name'] // 42 | name

我希望你现在更清楚了。