0

Is it possible to constrain a generic type T to the set of subtypes of type K not including K? I am trying to define a type for inheritance based mixin functions. Question 32488309 provides an answer for the opposite, and coincidentally this question is asked but unanswered in the comments.

TypeScript Playground

// Define a type for the constructor signature.
interface IConstructor { new(...args: any[]): any; }

type Mixin = <T extends IConstructor, U extends T>(Base: T) => U;

// Mix a set of mixins.
function mix(...mixins: Mixin[]) {
  return mixins.reduce((child: IConstructor, mixer) => mixer(child), Object);
}

// Mix typically accepts Mixins with the same constructor signature.
interface ILocalized extends IConstructor {
  new(i18n: I18n): any;
}

function mixinOne(Base: ILocalized) {
  return class MixinOne extends Base {
    constructor(i18n: I18n) { super(i18n); }
  }
}

This results in the following error from question 56505560 which explains that I have achieved the opposite of my goal. T cannot be the set of subtypes of K, because once it is MixinOne it can't be any other.

const LocalizedBase: IBase = mix(mixinOne, ...);

'typeof MixinOne' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'IConstructor'.

Alternatively, the following settles the error, but includes K despite the use of extends.

export type Mixin = (Base: IConstructor) => IConstructor;

I am not interested in the property iterating alternative solution for mixins, because these classes require dependency injection.

Trevor Karjanis
  • 1,485
  • 14
  • 25
  • I think a [mcve] would be useful here; specifically something that shows explicitly some calls to `mix()` that should succeed and other calls that should fail. Right now all I see is `mix(mixinOne)`, which can be satisfied in a number of ways, and it's not clear to me what role `K` should be taking (do note that `K` is conventionally used to mean a key-like type like `keyof T`; for a general type I'd switch to `U` instead). – jcalz Jan 18 '20 at 00:43
  • For example, [this](https://www.typescriptlang.org/play/#code/C4TwDgpgBAkgjADgHZQLxQN5QB4C4oDOwATgJZIDmUAvgNwCwAUOcBMQGYCGAxtDAMIB7JEWIBXbsEHFMUJBADuACgB0azsQoF8nJCADaAXQCUOvbRpNQkKAFlS2cgB5+UCNlZIAJgVhCRJBJSxAB8aFBKAEKcBBD4-MZoYfwMjOxiSJKkwlAAtg5OACpuHhDevgCCxMScIE72jkhOuiAhbapq+Y3aUIWJGExQUMQQwGLEKF3kBCojXhIQSkrcABakADZe+ALCokHSADR5DmyJqGFdbMtrm8ZHAPIARgBWEJLGqdRMLGxcvLDRWIlTw+Py7QKSaSYQZyRRKUiIJDbRGmKAtT5MdKZYDZSYOcj3eRRGJxAEk-owkZjCZQbjrGK+BoE+TAsqgwHQAaMIZDbjg8SQ4jwxHI5D9QhiSBChFiixfbmWRjyrpKKZIQkQD5AA) works, but without more test cases for `mix()` I'm not sure if that suffices. – jcalz Jan 18 '20 at 00:44
  • @jcalz I have updated the question with an example. Looking at yours, on line 4 I am not extending an array. I am extending each mixin class to produce a mixed final class. This is why I have defined the generics on Mixin. – Trevor Karjanis Jan 20 '20 at 16:51
  • The `extends Array` was just indicating that the `mix()` function takes a rest parameter of a generic array of mixin types, not that you were mixing into an array. Anyway, given your updated example, does [this](https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgJIGED2IDOYoCuCYmUyA3siBAO4AUAdE3FAOY4BcycIAngNoBdAJRcevANzIAvgFgAUGF4AHFAFlgAD1AAeVMgibIIACY40WXPiIkoAGmQB5A0YinzqAHzIAvMjoAQnA4EFyowr7ejhIKMAQgxMDYyAC2WnouxmYW2HiExKSejExp2rhcGmV6DvoAZBTSnkKiaBQKyMhQEGAEUCCpWqA4DF0mRBB0dAgAFsAANiYOpdARPt7LUFOzC8IOjgBGAFYQxNzm4mdowjHycvJAA) work for you? If so I'll write it up. – jcalz Jan 21 '20 at 00:40

1 Answers1

1

Here's my solution:

type RequireSubtype<A, B extends A> = A extends B ? never : B

To use it, write a generic function taking a type parameter B extends A, and an actual parameter of type RequireSubtype<A, B>. For example:

class Foo {
    constructor(public readonly x: number) {}
}

class Bar extends Foo {
    constructor(x: number, public readonly y: number) { super(x); }
}

function requireSubtype<T extends Foo>(arg: RequireSubtype<Foo, T>): void {
    // ...
}

// ok
requireSubtype(new Bar(2, 3));
// type error: Argument of type 'Foo' is not assignable to parameter of type 'never'.
requireSubtype(new Foo(1));

Playground Link

kaya3
  • 47,440
  • 4
  • 68
  • 97
  • 1
    Yeah, I was thinking `type StrictExtends = U;`, which is similar... but I don't know how the question intends to use it with `Mixin`, since that type evaluates to "a function which can take any constructor and produce any subtype of that constructor that the caller chooses" which is impossible or at least unsafe, and doesn't seem to be what the OP means. I think the generics should probably be on `mix()` and not on the `Mixin` type. Subtypes seem like a red herring here – jcalz Jan 17 '20 at 20:01
  • @jcalz This is a great observation, and of course, the one thing I left out. Mix typically accepts Mixins with the same constructor signature. Therefore, I use IConstructor for the Mixin type definition but extend it in practice. I updated the question to clarify. – Trevor Karjanis Jan 17 '20 at 20:13