在TypeScriptMap的trype中指定函数参数类型

jjhzyzn0  于 2023-06-07  发布在  TypeScript
关注(0)|答案(1)|浏览(208)

我有两个函数,它们接受一个options参数,除了用于标识函数的type之外,它们具有不同的属性。

type FuncAOptions = {
    type: 'A'
    opt1: string
    opt2: boolean
}

function funcA(options: FuncAOptions): number {
    if (!options.opt1) throw new Error('Missing required option')
    return 1
}

type FuncBOptions = {
    type: 'B'
    opt3: number
    opt4: (a: number) => number
}

function funcB(options: FuncBOptions): string {
    if (!options.opt3) throw new Error('Missing required option')
    return 'B'
}

然后,我就有了这些函数的Map,以及相关的Map类型,这样我就可以用可变的运行时数据有条件地调用这些函数。

type AllFunctions = FuncAOptions | FuncBOptions

type FunctionMap = { [K in AllFunctions['type']]: (options: any) => any; }

const functionMap: FunctionMap = {
    A: funcA,
    B: funcB
}

function callFunction(type: keyof typeof functionMap, options: any) {
    return functionMap[type](options)
}

当我直接调用函数时,我得到了正确的类型检查,以知道我是否传递了一组不正确的选项。我希望在通过中介方法调用函数时也能做到这一点。

callFunction('A', { type: 'B', opt3: 'Hello' }) // NO TS ERROR
funcA({ type: 'B', opt3: 'Hello' }) // TS ERROR: The expected type comes from property 'type' which is declared here on type 'FuncAOptions'

我喜欢使用K in AllFunctions['type']类型的Map,因为当我向AllFunctions添加函数时,我会提醒我需要向functionMap添加键值对。
完整的例子在这里

kkbh8khc

kkbh8khc1#

如果您希望functionMap[type](options)进行类型检查而不使用类型Assert或any类型,则需要将genericindexes的条件写入基本键值类型或该类型的mapped types。这是在microsoft/TypeScript#47109中描述的。
本质上,您希望将type视为某种泛型类型K,将functionMap视为某种Map类型(如{[P in keyof FuncOptionMap]: (arg: FuncOptionMap[P]) => FuncRetMap[P]}),将options视为类型FuncOptionMap[K]。然后编译器可以得出结论,函数functionMap[type]的类型为(arg: FuncOptionMap[K]) => FuncRetMap[K],因此可以用options作为参数调用,并将返回相应的返回类型FuncRetMap[K]。因此,我们需要根据您拥有的值定义FuncOptionMapFuncRetMap
您可能希望您可以在type仅仅是等价于keyof FuncOptionMap的联合类型而不需要泛型的情况下完成此操作。但是TypeScript不能遵循这种逻辑,如microsoft/TypeScript#30581中所述。推荐的方法是使用泛型索引到Map类型中;事实上,microsoft/TypeScript#47109是microsoft/TypeScript#30581的解决方案(或者至少是我们最接近的解决方案)。
它可以看起来像这样:

const _functionMap = {
    A: funcA,
    B: funcB
}

type FuncOptionMap =
    { [K in keyof typeof _functionMap]: Parameters<typeof _functionMap[K]>[0] }
type FuncRetMap =
    { [K in keyof typeof _functionMap]: ReturnType<typeof _functionMap[K]> }

const functionMap: { [K in keyof FuncOptionMap]: (options: FuncOptionMap[K]) => FuncRetMap[K] }
    = _functionMap

本质上,我们将functionMap重命名为_functionMap,然后将其分配回从它计算出的FuncOptionMapFuncRetMap类型。它看起来可能像一个no-op,因为_functionMap的类型和functionMap的类型看起来是相同的。但这其实很重要;编译器只能遵循当事物被写为该Map时的逻辑。如果在下面的代码中尝试使用_functionMap而不是functionMap,编译器将丢失线程并输出错误。
继续:

function callFunction<K extends keyof FuncOptionMap>(
    type: K, options: FuncOptionMap[K]
) {
    return functionMap[type](options); // okay
}

该类型检查,返回类型为FuncRetMap[K]。现在你得到了预期的错误:

callFunction('A', { type: 'B', opt3: 'Hello' }) // error!
// ---------------------> ~~~ B is not A

当你调用它时,编译器知道输出类型as如何依赖于输入类型:

console.log(callFunction(
    "B",
    { type: "B", opt3: 1, opt4: x => x + 1 }
).toLowerCase()); // okay, the compiler knows it's string and not number

Playground链接到代码

相关问题