Home > front end >  Typescript Generic Class Constraint
Typescript Generic Class Constraint

Time:09-17

I have the example below which describes a structure to pass to the Jumpable class. When i pass the Test class i get an error.

type GConstructor<T> = new (...args: any[]) => T;

type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;

function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      this.setPos(0, 20);
    }
  };
}

class Test {
  constructor() {
    console.log();
  }

  setPos(x: number, y: number) {
    console.log();
  }
}

Jumpable<Test>(new Test());

Type 'Test' does not satisfy the constraint 'Positionable'. Type 'Test' provides no match for the signature 'new (...args: any[]): { setPos: (x: number, y: number) => void; }'.ts(2344)

CodePudding user response:

It's important to understand the difference between JavaScript values which exist at runtime, and TypeScript types which exist only at compile time.

A type is kind of like a set of possible values. In the following code:

let x = "hello";

the variable x will hold the value "hello" at runtime, while the TypeScript compiler infers that it has the type string. And "hello" is one of the possible values that make up the string type, so that's fine. In the above, we could say "the type of x is string".

If you have a variable like x, you can ask TypeScript what its type is, by using the TypeScript typeof type query operator in a type context (not to be confused with the JavaScript typeof operator):

type TypeofX = typeof x;
// type TypeofX = string

Values are not types and types are not values. Much of the time there is a clear and obvious difference between values and types in TypeScript, or the difference is minor enough to ignore. For example, "singleton" or "unit" types that correspond to exactly one value are often given the same name as the value, like the string literal type "hello" corresponds to the value "hello", so you could get by just fine conflating one with the other.

But sometimes a value and a type can share a name but the value and type do not correspond directly to each other. The major situation where this happens is with class declarations.

In the following:

class Foo {
    x: string = "hello"
}

we have declared a class named Foo. At runtime, in JavaScript, this will create a value named Foo which is a class constructor. The value named Foo can be invoked as a constructor like new Foo(). TypeScript also knows about this value.

Additionally, the class declaration also brings into scope a TypeScirpt type named Foo, which corresponds to instances of the class. A value of type Foo should have an x property of type string.

And the type of the Foo value is not the Foo type. They are different. The type of the Foo value, typeof Foo, has a construct signature like new () => Foo. But the type named Foo has no such construct signature.

Observe:

const foo: Foo = new Foo();
const fooAlias: { x: string } = foo;

const fooCtor: typeof Foo = Foo;
const fooCtorAlias: new () => Foo = fooCtor;

const nope: Foo = Foo; // error! 
// Property 'x' is missing in type 'typeof Foo' but required in type 'Foo'
const alsoNope: typeof Foo = new Foo(); // error!
// Property 'prototype' is missing in type 'Foo' but required in type 'typeof Foo'

The foo and fooAlias variables are of type Foo or an equivalent type and hold an instance of the Foo class. The fooCtor and fooCtorAlias variables are of type typeof Foo or an equivalent type and hold the Foo class constructor. If you try to assign one to the other, you get errors.


So, let's look at your code:

type GConstructor<T> = new (...args: any[]) => T;

type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;

function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      this.setPos(0, 20);
    }
  };
}

Here, the Jumpable() function wants a Base parameter of type TBase which is constrained to Positionable, a type with a construct signature that constructs instances of type { setPos(x: number, y: number) => void }. A Positionable must be a class constructor. This all makes sense because Jumpable() returns a new class constructor that is a subclass of the passed-in Base. It looks like a mixin class factory.

Jumpable() wants its input to be a class constructor.

When you call this, though:

Jumpable<Test>(new Test());

You have passed it a value of type Test. Jumpable() doesn't want a value whose type is Test; that's a class instance, not a class constructor. You get the error:

Jumpable<Test>(new Test()); // error!
// Type 'Test' does not satisfy the constraint 'Positionable'.
// Type 'Test' provides no match for the signature 
// 'new (...args: any[]): { setPos: (x: number, y: number) => void; }'

which tells you exactly the problem: you are trying to use the instance type Test in a place that needs a constructor type. Test is not a constructor; it has no construct signature; it's wrong. If you compile and run that code, you'll get a runtime error too, like

//            
  • Related