Typescript能够执行简单的功能组合吗?

Ric*_*ter 1 functional-programming typescript typescript-generics typescript-typings

Typescript能够执行简单的功能组合吗?我写出了以下基本实现,以进行组合,映射和筛选以进行测试。

类型和功能在下面设置,然后在之后实现。

javascript似乎还不错,但是使用时打字稿显示出误报compose。具体而言,似乎可以理解第一个函数将要返回的内容,但随后它不会将该信息传递给第二个函数的输入。设置后在下面进一步解释。

import { curry } from './curry'

type f1<a, b> = (a: a) => b
type f2_2<a, b, c> = (a: a, b: b) => c
type f2_1<a, b, c> = (a: a) => (b: b) => c
type f2<a, b, c> = f2_2<a, b, c> & f2_1<a, b, c>
type p<a> = (a: a) => boolean

//====[compose]================================
type b3 = <a, b, c>(f: f1<b, c>, g: f1<a, b>, a: a) => c
type b2 = <a, b, c>(f: f1<b, c>, g: f1<a, b>) => f1<a, c>
type b1 = <a, b, c>(f: f1<b, c>) => f2<f1<a, b>, a, c>
type b = b1 & b2 & b3

// bluebird :: (b -> c) -> (a -> b) -> a -> c
const compose: b = curry((f, g, a) => f(g(a)))

//====[filter]=================================
type fil2 = <a>(p: p<a>, xs: a[]) => a[]
type fil1 = <a>(p: p<a>) => f1<a[], a[]>
type fil = fil1 & fil2

// filter :: (a -> Boolean) -> [a] -> [a]
const filter: fil = curry((p, xs) => {
  const len = xs.length
  const r = Array()

  for (let [i, j] = [0, 0]; i < len; i++) {
    const v = xs[i]

    if (p(v)) {
      r[j++] = v
    }
  }

  return r
})

//====[mapArr]=================================
type m2 = <a, b>(f1: f1<a, b>, xs: a[]) => b[]
type m1 = <a, b>(f: f1<a, b>) => f1<a[], b[]>
type m = m2 & m1

// map :: (a -> b) -> [a] -> [b]
const mapArr: m = curry((fn, xs) => {
  const len = xs.length
  const r = Array(len)

  for (let i = 0; i < len; i++) {
    r[i] = fn(xs[i])
  }

  return r
})

//====[prop]===================================
type z2 = <o, k extends keyof o>(propName: k, source: o) => o[k]
type z1 = <o, k extends keyof o>(propName: k) => f1<o, o[k]>
type z = z2 & z1

// prop :: String -> a -> b
// prop :: Number -> a -> b
// prop :: Symbol -> a -> b
const prop: z = curry((propName, obj) => obj[propName])
Run Code Online (Sandbox Code Playgroud)

当我将鼠标悬停在下面组成的filter函数上时,TS知道它将返回data[];但是,如果我将鼠标悬停在该mappArr函数上,TS将显示输入为unknown[],因此它将对该id字段引发误报。我究竟做错了什么?

//====[typescript test]===================================
interface data {
  relationId: string
  id: string
}

type isMine = p<data>
// isMine :: a -> Boolean
const isMine: isMine = x => x.relationId === '1'

type t = f1<data[], string[]>
const testFn: t = compose(
  // @ts-ignore
  mapArr(prop('id')),
  //=>       ^^^^^
  // error TS2345: Argument of type '"id"' is not assignable to
  // parameter of type 'never'.
  filter(isMine)
)

//====[javascript test]================================
const r = testFn([
  { id: '3', relationId: '1' },
  { id: '5', relationId: '3' },
  { id: '8', relationId: '1' },
])

test('javascript is correct', () => {
  expect(r).toEqual(['3', '8'])
})
Run Code Online (Sandbox Code Playgroud)

当我调用带有咖喱参数的compose时,Typescript的误报会变得更糟。

const testFn: t = compose (mapArr(prop('id')))
                          (filter(isMine))
//=>                      ^^^^^^^^^^^^^^^
// error TS2322: Type 'unknown[]' is not assignable to type 'f1<data[], string[]>'.
// Type 'unknown[]' provides no match for the signature '(a: data[]): string[]'.
Run Code Online (Sandbox Code Playgroud)

// ================================================ =====================

@shanonjackson想查看咖喱功能。我几乎肯定地说,它不可能成为任何问题的根源,因为上述每个函数的类型都应用于curry函数的结果,而不是通过它们,这似乎是不可能的。

再次,没有必要检查以下内容,这仅仅是的标准实现curry

function isFunction (fn) {
  return typeof fn === 'function'
}

const CURRY_SYMB = '@@curried'

function applyCurry (fn, arg) {
  if (!isFunction(fn)) {
    return fn
  }

  return fn.length > 1
    ? fn.bind(null, arg)
    : fn.call(null, arg)
}

export function curry (fn) {
  if (fn[CURRY_SYMB]) {
    return fn
  }

  function curried (...xs) {
    const args = xs.length
      ? xs
      : [undefined]

    if (args.length < fn.length) {
      return curry(Function.bind.apply(fn, [null].concat(args)))
    }

    const val =
      args.length === fn.length
        ? fn.apply(null, args)
        : args.reduce(applyCurry, fn)

    if (isFunction(val)) {
      return curry(val)
    }

    return val
  }

  Object.defineProperty(curried, CURRY_SYMB, {
    enumerable: false,
    writable: false,
    value: true,
  })

  return curried
}
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 7

无论我希望有多少,TypeScript都不是Haskell(或在此处插入您喜欢的类型的语言)。它的类型系统有很多漏洞,并且不能保证其类型推断算法会产生良好的结果。这并不是要证明是正确的(启用惯用JavaScript更重要)。公平地说,Haskell没有子类型化,面对子类型的类型推断要困难得多。无论如何,这里的简短答案是:

  • 不要试图依赖上下文类型(从函数输出中推断类型)
  • 尝试依靠前向类型推断(从函数输入中推断类型)
  • 当类型推断失败时,注释并指定类型。

由于TypeScript并不是Haskell,所以我要做的第一件事就是使用TypeScript命名约定。另外,由于的实现compose()和其余问题都不重要,因此我将仅介绍declare这些功能,而将实现遗漏在外。

让我们来看看:

type Predicate<A> = (a: A) => boolean;

//====[compose]===============================
declare function compose<B, C>(
  f: (x: B) => C
): (<A>(g: (a: A) => B) => (a: A) => C) & (<A>(g: (a: A) => B, a: A) => C); //altered
declare function compose<A, B, C>(f: (b: B) => C, g: (a: A) => B): (a: A) => C;
declare function compose<A, B, C>(f: (b: B) => C, g: (a: A) => B, a: A): C;

//====[filter]=================================
declare function filter<A>(p: Predicate<A>, xs: A[]): A[];
declare function filter<A>(p: Predicate<A>): (xs: A[]) => A[];

//====[mapArr]=================================
declare function mapArr<A, B>(f1: (a: A) => B): (xs: A[]) => B[];
declare function mapArr<A, B>(f1: (a: A) => B, xs: A[]): B[];

//====[prop]===================================
declare function prop<K extends keyof any>(
  propName: K
): <O extends Record<K, any>>(source: O) => O[K]; // altered
declare function prop<K extends keyof O, O>(propName: K, source: O): O[K];
Run Code Online (Sandbox Code Playgroud)

多数只是直接重写,但我想提请您注意我所做的两个实质性更改。第一个重载签名compose()已从更改为

declare function compose<A, B, C>(
  f: (x: B) => C
): ((g: (a: A) => B) => (a: A) => C) & ((g: (a: A) => B, a: A) => C);
Run Code Online (Sandbox Code Playgroud)

declare function compose<B, C>(
  f: (x: B) => C
): (<A>(g: (a: A) => B) => (a: A) => C) & (<A>(g: (a: A) => B, a: A) => C); 
Run Code Online (Sandbox Code Playgroud)

也就是说,与其compose()AB和中是通用的,而不是CB和中通用,C并返回的通用函数A。我这样做是因为在TypeScript中,通常根据传递给函数的参数的类型而不是函数的预期返回类型来推断泛型函数类型参数。是的,有上下文类型可以从所需的输出类型中推断出输入类型,但是它不如普通的“时间前向”推断可靠。

当调用一个泛型函数时,A如果没有任何参数充当其推理站点,可能会发生什么A?(例如,当它仅具有一个类型为type的参数时(x: B) => C)编译器将推断unknown从TS3.5开始)for A,您稍后将不满意。通过将通用参数规范推迟到调用返回的函数之前,我们更有可能推断A出您的预期方式。


同样,我已经改变的第一个重载prop()

declare function prop<K extends keyof O, O>(
  propName: K, 
): (source: O) => O[K];
Run Code Online (Sandbox Code Playgroud)

declare function prop<K extends keyof any>(
  propName: K
): <O extends Record<K, any>>(source: O) => O[K];
Run Code Online (Sandbox Code Playgroud)

这具有相同的问题...对类似的调用prop("id")将导致K被推断为"id",并且O很可能被推断为unknown,然后由于"id"不知道是keyof unknown(是never)的一部分,因此会出现错误。这很可能是引起您所看到的错误的原因。

无论如何,我已经将O调用返回函数的规范推迟到。这意味着我需要将通用约束从逆转K extends keyof OO extends Record<K, any>...说类似的话,但方向相反。


很好,如果我们尝试您的compose()测试会怎样?

//====[typescript test]===================================
interface Data {
  relationId: string;
  id: string;
}
type isMine = Predicate<Data>;
const isMine: isMine = x => x.relationId === "1";

const testFnWithFaultyContextualTyping: (a: Data[]) => string[] = compose(
  mapArr(prop("id")), // error!
  filter(isMine)
);
// Argument of type '<O extends Record<"id", any>>(source: O) => O["id"]' 
// is not assignable to parameter of type '(a: unknown) => any'.
Run Code Online (Sandbox Code Playgroud)

糟糕,仍然存在错误。这是一个不同的错误,但是这里的问题是,通过将您的返回值注释为(a: Data[]) => string[],它触发了一些上下文类型,这些类型在prop("id")正确推断之前逐渐消失。在这种情况下,我的本能是尝试不依赖于上下文类型,而是查看常规类型推断是否起作用:

const testFn = compose(
  mapArr(prop("id")),
  filter(isMine)
); // okay
// const testFn: (a: Data[]) => string[]
Run Code Online (Sandbox Code Playgroud)

因此,工程。时间前向推断的行为符合预期,并且testFn是您期望的类型。


如果我们尝试您的咖喱版本:

const testFnCurriedWithFaultyContextualTyping = compose(mapArr(prop("id")))( // error!
  filter(isMine)
); 
// Argument of type '<O extends Record<"id", any>>(xs: O[]) => O["id"][]' 
// is not assignable to parameter of type '(x: unknown) => any[]'.
Run Code Online (Sandbox Code Playgroud)

是的,我们仍然会出现错误。再次尝试进行上下文类型输入又是一个问题……编译器实际上并不真正知道如何根据给compose()定的调用其返回函数的方式来推断参数的类型。在这种情况下,无法通过移动通用类型来修复它。推断就不可能在这里发生。相反,我们可以依靠在compose()函数调用中显式指定泛型类型参数:

const testFnCurried = compose<Data[], string[]>(mapArr(prop("id")))(
  filter(isMine)
); // okay
Run Code Online (Sandbox Code Playgroud)

这样可行。


无论如何,我希望能给您一些思路,尽管可能会令人失望。每当我为TypeScript的不完善和有限的类型推断功能感到难过时,我都会提醒自己有关它的所有类型系统的巧妙功能,至少在我看来,这是对它的补充

无论如何,祝你好运!

链接到代码