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

TypeScript function that works on all numerical array types

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

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 编译器选项)。

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

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 中表达类似“我想要一个类型 Number | Float32 | ... 周围的通用 Wrapper”之类的东西。没有这 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, 2, 3].map(addOne);
Float32Array.from([1,2,3]).map(addOne);

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

TypeScript 没有内置的 NumericArray 类型,其中 Array<number>Float32Array-等等 是允许您访问所有常用方法的子类型。我也想不出可以为您提供的一两行解决方案。相反,如果您真的需要它,我建议您创建自己的类型。例如:

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, index: number, array: this) => any, thisArg?: any): this;
  find(predicate: (value: number, index: number, obj: this) => boolean, thisArg?: any): number | undefined;
  findIndex(predicate: (value: number, index: number, obj: this) => boolean, thisArg?: any): number;
  forEach(callbackfn: (value: number, index: 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, index: number, array: this) => number, thisArg?: any): this;
  reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number): number;
  reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number, initialValue: number): number;
  reduce<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: this) => U, initialValue: U): U;
  reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number): number;
  reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: this) => number, initialValue: number): number;
  reduceRight<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: this) => U, initialValue: U): U;
  reverse(): this;
  slice(start?: number, end?: number): this;
  some(predicate: (value: number, index: number, array: this) => unknown, thisArg?: any): boolean;
  sort(compareFn?: (a: number, b: number) => number): this;
  toLocaleString(): string;
  toString(): string;
  [index: number]: number;
}

有点长,但我通过合并 lib.es5.d.ts, and changing any reference to the particular array type with the polymorphic this type 中现有的数组类型来创建它,意思是“NumericArray 接口的当前子类型”。因此,例如,Array<number>map() 方法 return 是 Array<number>,而 Float32Arraymap() 方法 return 是 Float32Arraythis类型可以用来表示数组类型和return类型之间的这种关系。

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

您可以尝试 以编程方式编写计算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, 2, 3]));
// const float32Arr: this
console.log(float32Arr) // this: {0: 2, 1: 3, 2: 4}
console.log(float32Arr.buffer.byteLength); // 12

你的 doSomethingWithArray 看起来像这样:

function doSomethingWithArray<T extends NumericArray, R>(a: T, func: (a: T) => R) {
  return func(a);
}
console.log(doSomethingWithArray([4, 5, 6], x => x.unshift(3))); // 4
console.log(doSomethingWithArray(new Int8Array([1, 2, 3, 4, 5]), x => x.byteLength)); // 5

看起来不错!

Playground link to code

今天想起了这个问题,想出了一些我觉得有趣的东西:

function addOne <T extends NumericArray[number]> (a: T)
    : Narrow<T, NumericArray> {
        return a.map(x => x + 1) }

type NumericArray = [
    Array<number>,
    Int8Array,
    Uint8Array,
    Uint8ClampedArray,
    Int16Array,
    Uint16Array,
    Int32Array,
    Uint32Array,
    Float32Array,
    Float64Array
];

type Narrow<
    T, L extends unknown[] = [],
    R1 extends unknown[] = {
        [K in keyof L]: T extends L[K] ? { type: T } : never
    },
    R2 = unknown extends T ? never : R1[number],
> = [R2] extends [{ type: unknown }] ? R2['type'] : never

我喜欢它的可维护性配置文件与 jcalz 的答案不同,我仍然认为这对您的用例更有意义,因为 API 更简洁,您必须为此编写很多函数变体集。虽然这是很多繁重的工作,但如果我必须针对不同的变体集执行此操作,我会看到自己正在寻求第二种解决方案。

我想如果 Narrow 使用联合而不是元组,API 可能会更简洁一些,但我不知道添加此步骤的计算量有多大。