适用于所有数值数组类型的 TypeScript 函数

问题描述

我正在尝试编写一个适用于所有 JavaScript 数组类型的函数,例如在 number[]Float32Array 等上。它应该返回它作为参数获取的相同类型。一个简单的例子是:

function addOne<T>(a: T) : T {
  return a.map((x: number) => x + 1);
}

函数应该能够使用所有数组类型通用的所有方法(不仅仅是map)。

我也试过

type NumberArray<T> = T extends Array<number>
  ? Array<number>
  : Float32Array; // other array types skipped for brevity


function addOne<T>(a: NumberArray<T>): NumberArray<T> {
  return a.map((x: number) => x + 1);
}

但我明白

TS2322: Type 'number[] | Float32Array' is not assignable to type 'NumberArray<T>'.   Type 'number[]' is not assignable to type 'NumberArray<T>'.

这样一个函数的 TypeScript 签名是什么?我还希望能够创建几个这样的函数并将它们作为参数传递给另一个函数(当然,所有类型都正确)。一个简单的例子是:

function doSomethingWithArray(a,func) {
  return func(a);
}

a 的类型应定义使用 func 的哪个签名。 我在 JS 中运行它没有问题,但是当尝试添加正确的 TS 类型时,TS 编译器会抱怨(我正在使用 "strict": true 编译器选项运行)。

解决方法

TypeScript 没有内置的 NumericArray 类型,其中 Array<number>Float32Array-et-cetera 是子类型,可让您访问所有常用方法。我也想不出一个一两条线的解决方案可以给你。相反,如果您真的需要这个,我建议您创建自己的类型。例如:

interface NumericArray {
  every(predicate: (value: number,index: number,array: this) => unknown,thisArg?: any): boolean;
  fill(value: number,start?: number,end?: number): this;
  filter(predicate: (value: number,array: this) => any,thisArg?: any): this;
  find(predicate: (value: number,obj: this) => boolean,thisArg?: any): number | undefined;
  findIndex(predicate: (value: number,thisArg?: any): number;
  forEach(callbackfn: (value: number,array: this) => void,thisArg?: any): void;
  indexOf(searchElement: number,fromIndex?: number): number;
  join(separator?: string): string;
  lastIndexOf(searchElement: number,fromIndex?: number): number;
  readonly length: number;
  map(callbackfn: (value: number,array: this) => number,thisArg?: any): this;
  reduce(callbackfn: (previousValue: number,currentValue: number,currentIndex: number,array: this) => number): number;
  reduce(callbackfn: (previousValue: number,initialValue: number): number;
  reduce<U>(callbackfn: (previousValue: U,array: this) => U,initialValue: U): U;
  reduceRight(callbackfn: (previousValue: number,array: this) => number): number;
  reduceRight(callbackfn: (previousValue: number,initialValue: number): number;
  reduceRight<U>(callbackfn: (previousValue: U,initialValue: U): U;
  reverse(): this;
  slice(start?: number,end?: number): this;
  some(predicate: (value: number,thisArg?: any): boolean;
  sort(compareFn?: (a: number,b: number) => number): this;
  toLocaleString(): string;
  toString(): string;
  [index: number]: number;
}

这有点长,但我通过合并 lib.es5.d.ts 中现有的数组类型创建它,并使用 the polymorphic this type 更改对特定数组类型的任何引用,意思是“{{ 1}} 接口”。因此,例如,NumericArrayArray<number> 方法返回 map(),而 Array<number>Float32Array 方法返回 map()Float32Array 类型可用于表示数组类型和返回类型之间的这种关系。

如果您关心 ES5 后的功能,您也可以合并这些方法,但这应该足以演示基本方法。

您可以尝试编写一些以编程方式计算 this 的东西,但我不想这样做。与上面的手动 NumericArray 定义相比,它可能更脆弱、更令人困惑,并且可能需要几乎同样多的行。


然后,我会将您的 NumericArray 写成 addOne()

NumericArray

您可以验证它对 function addOne<T extends NumericArray>(a: T): T { return a.map((x: number) => x + 1); } Array<number> 是否按预期工作:

Float32Array

您的 const arr = addOne([1,2,3]); // const arr: number[] console.log(arr); // [2,3,4]; arr.unshift(1); // okay const float32Arr = addOne(new Float32Array([1,3])); // const float32Arr: this console.log(float32Arr) // this: {0: 2,1: 3,2: 4} console.log(float32Arr.buffer.byteLength); // 12 将如下所示:

doSomethingWithArray

看起来不错!

Playground link to code

,

你的最后一个问题有一个简单的解决方案

function apply<T,U>(a: T,func: (x: T) => U): U {
    return func(a);
}

更新

这似乎有效,但我看不出它有什么不安全之处。

type NumberArray = 
    { map:(f:(x:number) => number) => any } 
    & ArrayLike<number> 
    & Iterable<number>;


function addOne<T extends NumberArray>(a: T):T {
    return a.map((x: number) => x + 1)
}

我认为用 number[]Float32Array 做你想做的事情是不可能的,因为在 Javascript 中没有 Float32 类型这样的东西,我们不能表达像“我想要无论如何,在 Typescript 中围绕类型 Wrapper" 的通用 Number | Float32 | ...。没有这 2 个功能,我不知道怎么做。

我们可以做一些让 Typescript 开心的事情:

type NumberArray =
    {
        map: Function
        // define other methods there
    }
    & ArrayLike<number> 
    & Iterable<number>;


function addOne<T extends NumberArray>(a: T): T {
    return a.map((x: number) => x + 1)
}

但它允许您map到数字以外的类型:

function addOne<T extends NumberArray>(a: T): T {
    return a.map((x: number):string => x + '!') // woops
}

我们可以做到以下几点:

type NumberArray =
    {
        map:(f:(x:number) => number) => NumberArray
        // define other methods there
    }
    & ArrayLike<number> 
    & Iterable<number>;


function addOne<T extends NumberArray>(a: T): T {
    return a.map((x: number) => x + 1)
}

但是我们会得到一个错误。

Type 'NumberArray' is not assignable to type 'T'.
  'NumberArray' is assignable to the constraint of type 'T',but 'T' could be instantiated with a different subtype of constraint '{ map: (f: (x: number) => number) => NumberArray; } & ArrayLike<number> & Iterable<number>

它只能这样使用,这会丢失类型信息(也许对你来说没问题)

function addOne(a: NumberArray) {
    return a.map((x: number):number => x + 1)
}

顺便说一句,我看不到创建只能用于类似数组的对象的 addOne 函数的价值。这是非常具体的。 map 的全部意义在于将数据结构与转换解耦。

以下更灵活,因为 addOne 不需要知道它将在哪里使用并且类型信息被保留:

const addOne = (x: number) => x + 1;

addOne(3);
[1,3].map(addOne);
Float32Array.from([1,3]).map(addOne);

但它并没有解决您的问题:一旦我们定义了一个需要接受 NumberArray 作为参数的函数,我们就回到了开始的地方...