1

I'm writing a library to implement lazily evaluated queries of iterable types in JavaScript. I'm aware there are already available options that accomplish this, but mine is taking a drastically different approach to exposing the extension methods.

I'm having difficulty approaching a particular design challenge and was wondering if there was some work-around I hadn't thought of for resolving the issue.

My Enumerable<T> class definition and its interface along with a few helpers is as follows:

// Iterator<T> defined in lib.es2015.iterable.d.ts
interface IteratorFunction<T> {
  (this: IEnumerable<T> | void): Iterator<T>
}

interface CompareFunction<T> {
  (a: T, b: T): number
}

// inspired by pattern for ArrayConstructor in lib.es5.d.ts
interface IEnumerableConstructor {
  new <T>(iterator: IteratorFunction<T>): IEnumerable<T>
  new <T>(iterator: IteratorFunction<T>, compare: null): IEnumerable<T>
  new <T>(iterator: IteratorFunction<T>, compare: CompareFunction<T>): IOrderedEnumerable<T>

  // static methods...
}

// Symbol.compare defined in separate file as a declaration merge to SymbolConstructor
interface IOrderedEnumerable<T> extends IEnumerable<T> {
  readonly [Symbol.compare]: CompareFunction<T>

  // overrides for thenBy()...
}

// IterableIterator<T> defined in lib.es2015.iterable.d.ts
interface IEnumerable<T> extends IterableIterator<T> {
  // member methods...
}

class Enumerable<T> implements IEnumerableConstructor {
  readonly [Symbol.iterator]: IteratorFunction<T>
  readonly [Symbol.compare]: (null | CompareFunction<T>)

  constructor (iterator: IteratorFunction<T>)
  constructor (iterator: IteratorFunction<T>, compare: (null | CompareFunction<T>) = null) {
    this[Symbol.iterator] = iterator
    this[Symbol.compare] = compare
  }
}

The problem is that

constructor (iterator: IteratorFunction<T>)

refers to the hypothetical definition

new (iterator: IteratorFunction<T>): IEnumerable<T>

as opposed to the provided definition

new <T>(iterator: IteratorFunction<T>): IEnumerable<T>

and so on for the other overloaded declarations in IEnumerable<T>, providing me the error

Type 'Enumerable<T>' provides no match for the signature 'new <T>(iterator: IteratorFunction<T>): IEnumerable<T>'.

I considered changing the IEnumerableConstructor class to be generic as well, but then I run into a problem later when I try to do this:

declare global {
  // ...

  interface ArrayConstructor extends IEnumerableConstructor/*<T>*/ { } // illegal!
  interface MapConstructor extends IEnumerableConstructor { }
  interface SetConstructor extends IEnumerableConstructor { }
  interface StringConstructor extends IEnumerableConstructor { }
  interface TypedArrayConstructor extends IEnumerableConstructor { }

  interface Array<T> extends IEnumerable<T> { }
  interface Map<K, V> extends IEnumerable<[K, V]> { }
  interface Set<T> extends IEnumerable<T> { }
  interface String extends IEnumerable<string> { }
  interface TypedArray extends IEnumerable<number> { }
}

To clarify, I've declared TypedArray as the class that Int8Array and so on extend, as per the ECMAScript specification (but the typings for it are not provided in TypeScript lib.*.d.ts files since it's never directly used, which is understandable).

As I've said, this library implementation is a drastically different approach to exposing extension methods to built-ins in JavaScript and I'm aware of all the pitfalls, but right now I'm looking for a way to provide a definition for the class Enumerable<T> constructor that respects the generic type of IEnumerable<T>. Any ideas?

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153

1 Answers1

3

IEnumerableConstructor has construct signature, such interface can not be directly implemented by a class.

But in TypeScript, a class does not need to be explicitly declared as implementing some interface - after all, the type system is structural and you can just use your class in place where the interface is required, provided that it's compatible.

From what I understand in your code, Enumerable<T> must in fact implement IEnumerable<T>, because that's the type the construct signature in interface IEnumerableConstructor is declared to produce. So it should have next() method, and then it seems to work if you simply omit implements IEnumerableConstructor (but it wouldn't hurt if you replace it with implements IEnumerable, to make the compiler check that class implements that interface properly):

class Enumerable<T>  implements IEnumerable<T> {
  readonly [Symbol.iterator]: IteratorFunction<T>
  readonly compare: (null | CompareFunction<T>)

  constructor (iterator: IteratorFunction<T>)
  constructor (iterator: IteratorFunction<T>, compare?: (null | CompareFunction<T>)) {
    this[Symbol.iterator] = iterator
    this.compare = compare
    }

    next(): IteratorResult<T> {
        return undefined;
    }
}

const C: IEnumerableConstructor = Enumerable;

type A = { a: string };
let a: A[];

const c = new C(a.values); // inferred as const c: IEnumerable<A>
artem
  • 46,476
  • 8
  • 74
  • 78
  • Thanks, the methods weren't an issue, as I omitted those declarations and definitions for brevity. However, when I tried to get this to work, I had to remove `extends IterableIterator` from `interface IEnumerable`, otherwise there was a conflict assigning to `this[Symbol.iterator]`. I have yet to test if I can still pass an `IEnumerable` as the target of a `for...of` loop, but I can cross that bridge when I come to it. Thanks for this enlightening explanation! – Patrick Roberts Jun 28 '18 at 20:32
  • I've been working recently on this again, and I've realized a few important things. Using the builtin library definitions as a reference, it seems that the standard practice is to omit the `constructor` signature from `interface`s in favor of using the `new` signature in the interface's respective `...Constructor` interface. Secondly, I've noted that the sole purpose of `IEnumerable` is to define the subset of `Enumerable` that is [contravariant](https://flow.org/en/docs/lang/variance/#toc-contravariance) to `Array`, `Map`, `Set`, `String` and `TypedArray`. – Patrick Roberts Jun 25 '19 at 16:45