Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Igorbek/21b2bd503fd291d3281def829e2d5fbd to your computer and use it in GitHub Desktop.
Save Igorbek/21b2bd503fd291d3281def829e2d5fbd to your computer and use it in GitHub Desktop.
TypeScript Proposal: generic type constraint on member level

Examples from @niieani

Additional explanation and examples + possible usage taken from the duplicateMicrosoft/TypeScript#7083:

It's currently possible to do this:

  method<A extends Array<number>>(one):number;
  method<A extends Array<string>>(two):string;

But not possible to do this:

interface Example<T extends Array<number>> {
  method(one):number;
}
interface Example<T extends Array<string>> {
  method(two):string;
}

This means we cannot have different typings depending on the generic type constraint of the interface. I've even tried a hack like this:

interface Example<T extends Array<any>> {
  method<A extends T & Array<number>>(one):number;
  method<A extends T & Array<string>>(two):string;
}

I was hoping that TS will infer the type of the T array from the intersect, unfortunately that does not happen (it offers both methods, regardless of whether Example is an instance of Example<Array<number>> or of Example<Array<string>>). Among other uses, this feature would be useful for a variety of database libraries that return internal types that can have their own operations done on them. For example, when I map an DbArray<DbNumber>, I should get a different set of methods than when operating on DbArray<DbString>. The current workaround is to ask the user to explicitly declare both - the outer and the inner type, e.g.:

interface Example<TOuter, TInner> {
  map<TOut>((item: TInner) => TOut): TOut;
  // ...TOuter used in another method
}

However, this TInner is redundant (it's already inside of TOuter) and sometimes not relevant at all. With more complex types this can cause the introduction of even more generic types, that grow like cancer in the declarations (interface Example<A, B, C, D, ...>) - the more possibilities there are, the more explicit declarations need to be done at the interface level. This has been an obstacle to creating proper type definitions for RethinkDB DefinitelyTyped/DefinitelyTyped#4551. Last example: an interface DbValue can be a generic of any primitive type in the database: an array, a string or a number. For an instance of an array, you'd need to pass in DbValue<Array<number>, number>, yet for an instance of string, the second parameter is irrelevant: DbValue<string, void>. What we'd need is to be able to constrain not only by method's own generics, but by the containing interfaces generics, for method signatures themselves, something like:

interface Example<T> {
  // here the constraint is not of the method, but of the type
  method(one):number where T extends Array<number>;
  method(two):string where T extends Array<string>;
}

This is a suggestion for spec change.

Problem

Sometimes type's members could only be applicable with some restrictions of enclosing type parameter (generic type constraints).

For instance, this is use case of RxJS:

var xs: Observable<Observable<string>>;
xs.mergeAll();  // mergeAll is applicable here

var ys: Observable<string>;
ys.mergeAll();  // mergeAll is NOT applicable here

Original Rx.NET Merge method implemented as C# extension method on type IObservable<IObservable<T>>. In RxJS it's implemented as an instance method.

Current RxJS typescript definition just use unsafe trick:

export interface Observable<T> {
  ...
  mergeAll(): T;
  ...
}

But it can be still unsafely called for as instance of any Observable<T>.

Possible solutions

Extension methods

Something like C# extension methods. But don't think it's useful in such cases.

declare extensions {
  mergeAll<T>(this Observable<Observable<T>> source): Observable<T>;
}

BTW, extensions method could be other cool feature, which change call method (from instance-like to static-like).

Multiple interface definitions with different constraints

interface A<T> { x(): void; }
interface A<T extends B> { y(): void; }
var a: A<number>;
var b: A<B>;

a.x(); b.x(); // ok
b.y(); // ok
a.y(); // error

Member-level constraints with reference to enclosing type arguments

interface A<T> {
  x(): void;
  y<T extends B>(): void; // only applicable if T extends B
  z<T extends B>: number; // for any property
  // alternative syntax options:
  <T extends B>z: number;  // 1

  z: number where T extends B;  // 2
  y(): void where T extends B;
}

I'll suggest this option. Moreover the previous solution options is special case of this one (incompatible constraints are merged on member level).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment