打字稿-推断函数是否被处理

时间:2020-09-06 12:43:06

标签: typescript

我有一个将多参数“香草”功能转换为咖喱函数的功能。但是,如果传递单个参数函数,则将其保留为原始函数。 Typescript推断此函数转换函数的返回类型为vanilla函数和curried函数的并集。这很有意义,这就是两个可能的输出。

但是当我然后尝试调用由转换器函数返回的咖喱函数时,Typescript抱怨,因为它不知道我要呼叫的函数是咖喱返回还是简单返回。

下面是说明这一点的代码:

// Types for illustration
type VanillaFunction = (foo: string) => string
type CurriedFunction = (bar: number) => VanillaFunction
type FunctionToTransform = (arg1: string, ...otherArgs: any[])=>string

const oneArgFunction = (myString: string) => myString
const twoArgFunction = (myString: string, myNum: number) => myString + myNum.toString()

const functionTransformer = (myFunction: FunctionToTransform) => {
    if(myFunction.length === 1) {
        return (arg1: string) => myFunction(arg1)
    } else {
        return (...otherArgs: any[]) => (arg1:string) => myFunction(arg1, ...otherArgs)
    }
}

const vanillaFunction = functionTransformer(oneArgFunction)
const curriedFunction = functionTransformer(twoArgFunction)

console.log(vanillaFunction("hello"))
//error here because TS doesn't know whether this is simple function or a curried function
console.log(curriedFunction(2)("hello"))

现在,我可以通过明确地告诉TS退货的类型来解决它,即:

const vanillaFunction = functionTransformer(oneArgFunction) as VanillaFunction
const curriedFunction = functionTransformer(twoArgFunction) as CurriedFunction

但是有没有办法使编译器推断是正确的类型?

更新:

再多考虑一下,似乎类型卫士应该通过帮助TS区分转换函数的输入和输出来帮助我。所以我尝试了这个:

// Types for illustration
type VanillaFunction = (arg1: string) => string
type CurriedFunction = (arg2: any, ...otherArgs: any[]) => VanillaFunction
type FunctionToTransformSingleArg = (arg1: string)=>string
//Adding the additional non-optional argument is necessary to make FunctionToTransformSingleArg incompatabile
//otherwise the type guard won't work
type FunctionToTransformWithExtra = (arg1: string, arg2: any, ...otherArgs: any[])=>string
type FunctionToTransform = FunctionToTransformSingleArg | FunctionToTransformWithExtra

function isFunctionToTransformSingleArg(functionToTransform: FunctionToTransform): functionToTransform is FunctionToTransformSingleArg {
    return functionToTransform.length === 1
}

function isCurriedFunction(ambiguousFunction: VanillaFunction | CurriedFunction): ambiguousFunction is CurriedFunction {
    return typeof ambiguousFunction("test string") === "function"
}

const oneArgFunction:FunctionToTransform = (myString: string) => myString
const twoArgFunction:FunctionToTransform = (myString: string, myNum: number) => myString + myNum.toString()

const functionTransformer = (myFunction: FunctionToTransform) => {
    let returnFunction: VanillaFunction | CurriedFunction
    if(isFunctionToTransformSingleArg(myFunction)) {
        returnFunction = (arg1: string) => myFunction(arg1)
    } else {
        returnFunction = (arg2: any, ...otherArgs: any[]) => (arg1:string) => myFunction(arg1, arg2, ...otherArgs)
    }
    return isCurriedFunction(returnFunction) ? returnFunction : returnFunction
}

const vanillaFunction = functionTransformer(oneArgFunction)
const curriedFunction = functionTransformer(twoArgFunction) 

console.log(vanillaFunction("hello"))
console.log(curriedFunction(2)("hello"))
//              ^ -- Error here:  "Type 'string' has no call signature"

但是正如您所看到的,尽管有类型保护,但由于某种原因,它仍无法为curried函数推断正确的类型。似乎在推断CurriedFunction类型的返回类型,而不是完整类型。有什么想法吗?

代码也位于this playground上。

1 个答案:

答案 0 :(得分:0)

我知道了。上面的类型保护方法的问题在于,我试图在函数 return 上使用类型保护。我现在意识到的问题是,它实际上并没有帮助编译器。当然,它告诉编译器如何区分将要返回的两种类型的函数,但是(在没有更多信息的情况下)直到运行时才知道采用哪种路径将两种类型的返回函数中的一种返回。

结果的关键是给编译器一些更多的信息,有效地告诉它:“您可以从输入类型中找出函数的返回类型”。换句话说,一个基于输入类型的条件类型(由 input 类型的类型防护缩小,而不是返回类型)。

基本上,您是在告诉编译器,如果函数的输入x是Y类型(由防护确定),则返回类型将是A类型,如果输入x是Z类型(同样由防护确定) ),则返回类型将为B。

这也由于以下事实而复杂化:一个功能签名与另一个功能签名兼容,因此如果没有一些名义上的键入/标记,条件类型将无法工作。

这是解决方案(但是我的星期日去了哪里!):

// These are the types we ultimately want TS to distinguish between: plain functions and curried
type VanillaFunction = (arg1: string) => string
type CurriedFunction = (arg2: any, ...otherArgs: any[]) => VanillaFunction

// This branding is needed to make the two function signatures incompatible.  Normally a function
// having fewer parameters is compatible with one more arguments, for good reason.
// See: https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-functions-with-fewer-parameters-assignable-to-functions-that-take-more-parameters
type FunctionToTransform = ((arg1: string) => string) | ((arg1: string, ...otherArgs: any[])=>string)
type FunctionToTransformSingleArg = {type: "single", function: (arg1: string)=>string}
type FunctionToTransformWithExtra = {type: "multiple", function: (arg1: string, ...otherArgs: any[])=>string}
type FunctionToTransformBranded = FunctionToTransformSingleArg | FunctionToTransformWithExtra

// Type guard for the function we are passing in
function isFunctionToTransformSingleArg(functionToTransform: FunctionToTransformBranded): functionToTransform is FunctionToTransformSingleArg {
    return functionToTransform.type === "single"
}

// Creator functions to tell TS whether we are passing in a function with one argument or 
// more via type branding (see above).  Otherwise, our conditional type below wouldn't work 
// because everything would resolve to the same type-compatible signature for the multi-argument 
// function
function createOneArgFunction(fn: FunctionToTransform):FunctionToTransformSingleArg {
    return {type: "single", function:fn}
}
function createTwoArgFunction(fn: FunctionToTransform): FunctionToTransformWithExtra {
    return {type: "multiple", function: fn}
}

const oneArgFunction = createOneArgFunction((myString: string) => myString)
const twoArgFunction = createTwoArgFunction((myString: string, myNum: number) => myString + myNum.toString())

const functionTransformer = <T extends FunctionToTransformBranded>(myFunction: T) => {
    let returnFunction
    if(isFunctionToTransformSingleArg(myFunction)) {
        returnFunction = (arg1: string) => myFunction.function(arg1)
    } else {
        returnFunction = ( ...otherArgs: any[]) => (arg1:string) => myFunction.function(arg1, ...otherArgs)
    }
    // Conditional type that tells the compiler it can infer the return type on the basis of the input type, as
    // narrowed by the type guard
    return returnFunction as typeof myFunction extends FunctionToTransformSingleArg ? VanillaFunction : CurriedFunction
}

const vanillaFunction = functionTransformer(oneArgFunction)
const curriedFunction = functionTransformer(twoArgFunction) 

console.log(vanillaFunction("hello"))
console.log(curriedFunction(2)("hello"))

console.log(vanillaFunction("hello")("hello"))
//                  ^--  Properly errors because this not a curried function
//                       and will fail at runtime

这是a playground中的代码。