在TypeScript中键入编写函数(Flow $Compose)

ahy6op9u  于 2023-01-10  发布在  TypeScript
关注(0)|答案(6)|浏览(116)

In flow there is support for $Compose functions (see recompose as example). However, I can't seem to find such a mechanism in typescript. it seems that the best typescript can do is something like https://github.com/reactjs/redux/blob/master/index.d.ts#L416-L460. What's the equivalent to $Compose in Typescript?
编辑:我尝试完成的是从recomposeredux中输入compose函数,使其类型安全,特别是,对于react高阶组件,我想确保一个HOC的输出属性满足下一个HOC的输入属性。这是我目前的工作方法,似乎工作得相当好-尽管我希望有一个很好的方法在 typescript 中原生地完成这一点。

/** Wraps recompose.compose in a type-safe way */
function composeHOCs<OProps, I1, IProps>(
  f1: InferableComponentEnhancerWithProps<I1, OProps>,
  f2: InferableComponentEnhancerWithProps<IProps, I1>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs<OProps, I1, I2, IProps>(
  f1: InferableComponentEnhancerWithProps<I1, OProps>,
  f2: InferableComponentEnhancerWithProps<I2, I1>,
  f3: InferableComponentEnhancerWithProps<IProps, I2>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs<OProps, I1, I2, I3, IProps>(
  f1: InferableComponentEnhancerWithProps<I1, OProps>,
  f2: InferableComponentEnhancerWithProps<I2, I1>,
  f3: InferableComponentEnhancerWithProps<I3, I2>,
  f4: InferableComponentEnhancerWithProps<IProps, I3>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs(
  ...fns: Array<InferableComponentEnhancerWithProps<any, any>>
): ComponentEnhancer<any, any> {
  return compose(...fns)
}
ss2ws0br

ss2ws0br1#

我把你的问题解读如下:
我如何给予这个高阶函数一个TS类型,这样x的类型就可以在循环中变化?

function compose(...funs) {
    return function(x) {
        for (var i = funs.length - 1; i >= 0; i--) {
            x = funs[i](x);
        }
        return x;
    }
}

坏消息是你不能直接输入这个函数,funs数组就是问题所在--给予compose一个最通用的类型,funs应该是一个 * 类型对齐 * 的函数列表-每个函数的输出必须与下一个函数的输入匹配。TypeScript的数组是同构类型的-funs的每个元素必须具有完全相同的类型-因此您不能直接在TypeScript中表达类型在整个列表中变化的方式。(上面的JS在运行时工作,因为类型被擦除,数据被统一表示。)这就是Flow的$Compose是一个特殊的内置类型的原因。
解决此问题的一个方法是执行您在示例中所做的操作:使用不同数量的参数为compose声明一组重载。

function compose<T1, T2, T3>(
    f : (x : T2) => T3,
    g : (x : T1) => T2
) : (x : T1) => T3
function compose<T1, T2, T3, T4>(
    f : (x : T3) => T4,
    g : (x : T2) => T3,
    h : (x : T1) => T2
) : (x : T1) => T4
function compose<T1, T2, T3, T4, T5>(
    f : (x : T4) => T5,
    g : (x : T3) => T4,
    h : (x : T2) => T3,
    k : (x : T1) => T2
) : (x : T1) => T5

很明显,这是不可伸缩的,你必须停下来,如果你的用户需要编写比你预期的更多的函数,那他们就惨了。
另一种选择是重写代码,以便一次只组合一个函数:

function compose<T, U, R>(g : (y : U) => R, f : (x : T) => U) : (x : T) => R {
    return x => f(g(x));
}

这相当混乱了调用代码--您现在必须编写单词compose及其伴随的括号,O(n)次。

compose(f, compose(g, compose(h, k)))

像这样的函数组合管道在函数语言中很常见,那么程序员如何避免这种语法上的不适呢?例如,在Scala中,compose是一个 * 中缀 * 函数,这使得嵌套的括号更少。

f.compose(g).compose(h).compose(k)

在 haskell 语中,compose拼写为(.),这使得句子非常简洁:

f . g . h . k

事实上,你可以在TS中组装一个中缀compose,其思想是用一个执行组合的方法将底层函数 Package 在一个对象中,你可以将该方法称为compose,但我将其称为_,因为它的噪音较小。

class Comp<T, U> {
    readonly apply : (x : T) => U

    constructor(apply : (x : T) => U) {
        this.apply = apply;
    }

    // note the extra type parameter, and that the intermediate type T is not visible in the output type
    _<V>(f : (x : V) => T) : Comp<V, U> {
        return new Comp(x => this.apply(f(x)))
    }
}

// example
const comp : (x : T) => R = new Comp(f)._(g)._(h)._(k).apply

仍然没有compose(f, g, h, k)那么整洁,但它也不太可怕,而且它比编写大量重载更好地扩展。

toe95027

toe950272#

从Typescript 4开始,可变元组类型提供了一种组合函数的方法,函数的签名可以从任意数量的输入函数中推断出来。

let compose = <T, V>(...args: readonly [
        (x: T) => any,          // 1. The first function type
        ...any[],               // 2. The middle function types
        (x: any) => V           // 3. The last function type
    ]): (x: V) => T =>          // The compose return type, aka the composed function signature
{
    return (input: V) => args.reduceRight((val, fn) => fn(val), input);
};

let pipe = <T, V>(...args: readonly [
        (x: T) => any,          // 1. The first function type
        ...any[],               // 2. The middle function types
        (x: any) => V           // 3. The last function type
    ]): (x: T) => V =>          // The pipe return type, aka the composed function signature
{
    return (input: T) => args.reduce((val, fn) => fn(val), input);
};

然而,这种实现方式仍然存在两个缺点:
1.编译器无法验证每个函数的输出是否与下一个函数的输入匹配
1.编译器在使用spread运算符时发出抱怨(但仍能成功推断合成签名)
例如,以下代码将在编译时和运行时工作

let f = (x: number) => x * x;
let g = (x: number) => `1${x}`;
let h = (x: string) => ({x: Number(x)});

let foo = pipe(f, g, h);
let bar = compose(h, g, f);

console.log(foo(2)); // => { x: 14 }
console.log(bar(2)); // => { x: 14 }

虽然这将在运行时抱怨,但正确推断签名并运行

let fns = [f, g, h];
let foo2 = pipe(...fns);

console.log(foo2(2)); // => { x: 14 }
3wabscal

3wabscal3#

下面是TypeScript中强类型组合函数的一个例子,它的缺点是不检查每个中间函数类型,但是它能够为最终组合函数导出arg和返回类型。

    • 合成函数**
/** Helper type for single arg function */
type Func<A, B> = (a: A) => B;

/**
 * Compose 1 to n functions.
 * @param func first function
 * @param funcs additional functions
 */
export function compose<
  F1 extends Func<any, any>,
  FN extends Array<Func<any, any>>,
  R extends
    FN extends [] ? F1 :
    FN extends [Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, any, any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    Func<any, ReturnType<F1>> // Doubtful we'd ever want to pipe this many functions, but in the off chance someone does, we can still infer the return type
>(func: F1, ...funcs: FN): R {
  const allFuncs = [func, ...funcs];
  return function composed(raw: any) {
    return allFuncs.reduceRight((memo, func) => func(memo), raw);
  } as R
}
    • 示例用法:**
// compiler is able to derive that input type is a Date from last function
// and that return type is string from the first
const c: Func<Date, string> = compose(
  (a: number) => String(a),
  (a: string) => a.length,
  (a: Date) => String(a)
);

const result: string = c(new Date());
    • 工作原理**我们在一个函数数组上使用reduceRight,从最后一个到第一个为每个函数提供输入。对于composite的返回类型,我们可以根据最后一个函数的参数类型推断参数类型,并根据第一个函数的返回类型推断最后一个函数的返回类型。
    • 管道功能**

我们也可以创建一个强类型管道函数,将数据从第一个函数传输到下一个函数,以此类推。

/**
 * Creates a pipeline of functions.
 * @param func first function
 * @param funcs additional functions
 */
export function pipe<
  F1 extends Func<any, any>,
  FN extends Array<Func<any, any>>,
  R extends
    FN extends [] ? F1 :
    F1 extends Func<infer A1, any> ?
      FN extends [any] ? Func<A1, ReturnType<FN[0]>> :
      FN extends [any, any] ? Func<A1, ReturnType<FN[1]>> :
      FN extends [any, any, any] ? Func<A1, ReturnType<FN[2]>> :
      FN extends [any, any, any, any] ? Func<A1, ReturnType<FN[3]>> :
      FN extends [any, any, any, any, any] ? Func<A1, ReturnType<FN[4]>> :
      Func<A1, any> // Doubtful we'd ever want to pipe this many functions, but in the off chance someone does, we can infer the arg type but not the return type
    : never
>(func: F1, ...funcs: FN): R {
  const allFuncs = [func, ...funcs];
  return function piped(raw: any) {
    return allFuncs.reduce((memo, func) => func(memo), raw);
  } as R
}
    • 示例用法**
// compile is able to infer arg type of number based on arg type of first function and 
// return type based on return type of last function
const c: Func<number, string> = pipe(
  (a: number) => String(a),
  (a: string) => Number('1' + a),
  (a: number) => String(a)
);

const result: string = c(4); // yields '14'
kg7wmglp

kg7wmglp4#

TypeScript 4的元组类型改进可以用于pipecompose函数类型,而无需定义覆盖列表。
编译器将确保每个函数都能像预期的那样被下面的函数调用(每个中间函数都进行了类型检查)。

type UnaryFunction = (x: any) => any

type Composable<Fn> = 
  Fn extends readonly [UnaryFunction] ? Fn :
  Fn extends readonly [any, ...infer Rest extends readonly UnaryFunction[]] ?
    readonly [(arg: ComposeReturn<Rest>) => any, ...Composable<Rest>, ] : never 
 
type ComposeReturn<Fns extends readonly UnaryFunction[]> = ReturnType<Fns[0]>

type ComposeParams<Fns> = Fns extends readonly [...any[], infer Last extends UnaryFunction] ?
  Parameters<Last>[0] : never

function compose<Fns extends readonly UnaryFunction[]>(...fns: Composable<Fns>) {
  return function(arg: ComposeParams<Fns>): ComposeReturn<Fns> {
    return fns.reduceRight((acc, cur) => cur(acc), arg) as ComposeReturn<Fns>;
  }
}

示例:

function add3(x: number): number {
  return x + 3
}

function uppercase(x: string): string {
  return x.toUpperCase();
}

function stringify(x: number): string {
  return x.toString();
}

const composed = compose(
  uppercase,
  stringify,
  add3,
);
console.log(composed(0));

一个值得注意的限制是TypeScript仍然不能推断泛型函数的Parameter和Return类型。从TypeScript4.7开始,您可以通过使用示例化表达式来帮助编译器。

function add3(x: number): number {
  return x + 3
}

function stringify(x: number): string {
  return x.toString();
}

function identity<T>(t: T): T {
  return t;
}

const composed = compose(
  stringify,
  // have to use Instantiation Expressions from TS 4.7 when using generics
  identity<string>,
  add3,
);
console.log(composed(0));
fsi0uk1n

fsi0uk1n5#

我发现写一个类型化的compose函数并不太难now(TypeScript v4.1.5及以上版本,在TypeScript Playground测试过),这里有一个例子,它可以检查每个中间函数类型。

type Compose<F> =
    (F extends [infer F1, infer F2, ...infer RS] ?
        (RS extends [] ?
            (F1 extends (...args: infer P1) => infer R1 ?
                (F2 extends (...args: infer P2) => infer R2 ?
                    ([R1] extends P2 ?
                        (...args: P1) => R2 :
                        never) :
                    never) :
                never) :
            Compose<[Compose<[F1, F2]>, ...RS]>) :
        never);

type ComposeArgs<T> = Parameters<Compose<T>>;
type ComposeReturn<T> = ReturnType<Compose<T>>;

// I forget that composition is from right to left! 
type Reverse<T extends unknown[], RE extends unknown[] = []> = T extends [infer F, ...infer RS] ? Reverse<RS, [F, ...RE]> : RE;

function composeL2R<T extends Function[]>(...fns: T): (...args: ComposeArgs<T>) => ComposeReturn<T> {
    return (...args: ComposeArgs<T>): ComposeReturn<T> => fns.reduce((acc: unknown, cur: Function) => cur(acc), args);
}

function compose<T extends Function[]>(...fns: T): (...args: ComposeArgs<Reverse<T>>) => ComposeReturn<Reverse<T>> {
    return (...args: ComposeArgs<Reverse<T>>): ComposeReturn<Reverse<T>> => fns.reduceRight((acc: unknown, cur: Function) => cur(acc), args);
}

function fns(x: number): string { return `${x}0`; }
function fnn(x: number): number { return 2 * x; }
function fsn(x: string): number { return parseInt(x); }

let aNumber = compose(fsn, fns, fnn, fsn, fns, () => 1)();
let aNumberL2R = composeL2R(() => 1, fns, fsn, fnn, fns, fsn)();
let aNever = composeL2R(fnn, fsn, fns)(1);
let aNeverL2R = composeL2R(fnn, fsn, fns)(1);
yi0zb3m4

yi0zb3m46#

我做了一些挖掘,发现了一个很棒的递归解决方案,在注解区的“@cartersnook6139”中,对Matt Pocock's video的一个类型化的compose函数进行了递归。这里有一个到Typescript Playground的链接。它太神奇了!

declare const INVALID_COMPOSABLE_CHAIN: unique symbol;

type Comp = (arg: any) => any;

type IsValidChain<T extends ((arg: never) => any)[]> =
    T extends [infer $First extends Comp, infer $Second extends Comp, ...infer $Rest extends Comp[]]
        ? [ReturnType<$First>] extends [Parameters<$Second>[0]]
            ? IsValidChain<[$Second, ...$Rest]>
        : (T extends [any, ...infer $Rest] ? $Rest["length"] : never)
    : true;

type ReplaceFromBack<T extends unknown[], Offset extends number, Item, $Draft extends unknown[] = []> =
    $Draft["length"] extends Offset
        ? $Draft extends [any, ...infer $After]
            ? [...T, Item, ...$After]
        : never
    : T extends [...infer $Before, infer $Item]
        ? ReplaceFromBack<$Before, Offset, Item, [$Item, ...$Draft]>
    : never;

type asdf = ReplaceFromBack<[1, 2, 3, 4, 5, 6, 7, 8, 9], 3, "hey">;

function compose<Composables extends [Comp, ...Comp[]]>(
  ...composables:
        IsValidChain<Composables> extends (infer $Offset extends number)
            ? ReplaceFromBack<Composables, $Offset, "INVALID_COMPOSABLE">
        : Composables
) {
  return (
    firstData: Parameters<Composables[0]>[0]
  ): Composables extends [...any[], infer $Last extends (arg: never) => any]
    ? ReturnType<$Last>
    : never => {
    let data: any = firstData;
    for (const composable of composables) {
      data = (composable as any)(data);
    }
    return data;
  };
}

const addOne = (a: number): number => a + 1;
const numToString = (a: number): string => a.toString();
const stringToNum = (a: string): number => parseFloat(a);

namespace CorrectlyPassing {
  const v0 = compose(addOne, numToString, stringToNum); 
  //    ^?

  const v1 = compose(addOne, addOne, addOne, addOne, addOne, numToString);
  //    ^?

  const v2 = compose(numToString, stringToNum, addOne);
  //    ^?

  const v3 = compose(addOne, addOne, addOne);
  //    ^?
}

namespace CorrectlyFailing {
  // :o they actually show the error next to the incorrect one!
  compose(addOne, stringToNum);
  compose(numToString, addOne);
  compose(stringToNum, stringToNum);
  compose(addOne, addOne, addOne, addOne, stringToNum);
  compose(addOne, addOne, addOne, addOne, stringToNum, addOne);
}

相关问题