Typescript-함수가 커리인지 유추

Sep 06 2020

다중 인수 "바닐라"함수를 카레 함수로 변환하는 함수가 있습니다. 그러나 단일 인수 함수를 전달하면 바닐라 함수로 유지됩니다. Typescript는이 함수 변환 함수의 반환 유형을 바닐라 함수와 카레 함수의 합집합으로 추론합니다. 이는 두 가지 가능한 출력입니다.

그러나 변환기 함수에서 반환 된 커리 함수를 호출하려고하면 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"

그러나 보시다시피 어떤 이유로 typeguard에도 불구하고 curried 함수에 대해 올바른 유형을 추론하지 않습니다. 전체 유형이 아닌 CurriedFunction 유형의 반환 유형을 추론하는 것 같습니다. 이유는 무엇입니까?

코드 도이 놀이터 에 있습니다.

답변

sam256 Sep 07 2020 at 01:18

나는 그것을 알아. 위의 유형 가드 접근 방식의 문제점은 함수 return 에 유형 가드를 사용하려고한다는 것 입니다. 이제 문제는 컴파일러에 실제로 도움이되지 않는다는 것입니다. 확실히 그것은 컴파일러에게 반환 될 두 가지 유형의 함수를 구별하는 방법을 알려 주지만 (추가 정보없이) 두 가지 유형의 반환 함수 중 하나에 도달하기 위해 어떤 경로를 택했는지는 런타임까지 알 수 없습니다.

이것이 밝혀낸 핵심은 컴파일러에게 더 많은 정보를 제공하여 "입력 유형에서 함수의 반환 유형을 파악할 수 있습니다"라고 효과적으로 알려주는 것입니다. 즉, 입력 유형을 기반으로하는 조건부 유형 ( 반환 유형이 아닌 입력 유형 에 대한 유형 보호에 의해 좁혀 짐 ).

기본적으로 함수에 대한 입력 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

다음 은 놀이터 의 코드입니다 .