TypeScript: Regression: 2.4.1 Generic type mismatch

This is a simplified piece of code which demonstrates the problem. It compiles fine in 2.3.4 & 2.4.0, but produces a not assignable error in 2.4.1 and nightly.

TypeScript Version: 2.4.1 / nightly (2.5.0-dev.20170629)

Code

interface MyInterface {
  something: number;
}

class MyClass<T extends MyInterface> {
  doIt(data : Partial<T>) {}
}

class MySubClass extends MyClass<MyInterface> {}

function fn(arg: typeof MyClass) {};

fn(MySubClass);

Expected behavior:

No compile error.

Actual behavior:

test.ts(13,4): error TS2345: Argument of type 'typeof MySubClass' is not assignable to parameter of type 'typeof MyClass'.
  Type 'MySubClass' is not assignable to type 'MyClass<T>'.
    Types of property 'doIt' are incompatible.
      Type '(data: Partial<MyInterface>) => void' is not assignable to type '(data: Partial<T>) => void'.
        Types of parameters 'data' and 'data' are incompatible.
          Type 'Partial<T>' is not assignable to type 'Partial<MyInterface>'.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 37 (14 by maintainers)

Most upvoted comments

This fixes the urgent symptom of the problem, but doesn’t seem to address the underlying problem. To wit: the new generic checks don’t seem to play nicely with class generics and subclasses. My entire data model is predicated on having a generic Model<DataSchema> class to represent a data model and its actions (get / set / reset / etc), and then the user subclassing Model with a concrete instance of Schema, so we know the specific shape of the data returned by get().

I’d prefer to write canonical TS, not stuff that only builds if you turn off default compiler settings.

An extends clause requires its argument to be a type reference (a type name possibly followed by type arguments in angle brackets), but you can always satisfy this syntactic restriction by declaring a type alias for the particular type. However, even if you extend (an alias for) typeof UIComponent you still end up with the constructor signatures from that type, and new ones you add just become additional overloads. But that you could work around by applying a mapped type (which filters out all non-public members and all call and construct signatures). So, this appears to do the trick:

type PublicMembersOf<T> = { [P in keyof T]: T[P] };

interface ComponentFactory extends PublicMembersOf<typeof UIComponent> {
    new(): UIComponent<HTMLElement>;
}

@mat3e

// ...

+declare class AnyTestBase extends TestBase<DefinitionBase> {
+  definition: DefinitionBase;
+};

// ...

-class AllTestClasses<T extends typeof TestBase> {
+class AllTestClasses<T extends typeof AnyTestBase> {
  array: T[];
}

// ...

@estaub

TestBase<DefinitionBase> is an instance type (members = properties)

typeof TestBase is a class type (members = static properties + constructor)

Hi,

since TS 2.4.1, I’m facing errors when using type inference with generic functions.

Example:

type transformerFn = <T, U>(input: T) => U;
const toUpper: transformerFn = (str: string): string => str.toUpperCase();

Expected result: Both, T and U should be inferred to be type string.

Actual result: tsc produces the following output:

error TS2322: Type ‘(str: string) => string’ is not assignable to type ‘transformerFn’. Types of parameters ‘str’ and ‘input’ are incompatible. Type ‘T’ is not assignable to type ‘string’.

Affected versions: 2.4.1 & 2.42

Workaround: I can workaround the described error by setting noStrictGenericChecks to true.

I use this pattern quite often to describe a common type interface, which can be configured with concrete implementations. And therefore, I relying on the type inference system in these cases.

Can someone please tell me if this is related to this issue or if I should open a new issue for the problem, of if this is an error at all!?

Thanks in advance 😄

@ericeslinger - yeah, definitely looks like the same kind of problem.

Using any is not a solution I would like to use 😃

@ikatyang , Mmm, thank you for your explanation, I think I have some misunderstanding about typeof operator in TS. Maybe in my case I should change the code to:

class Base<P = {}> {
  constructor(protected p: P) { }
}

class Baz<P = {}> extends Base<P> { }

const factory = (Clazz: typeof Base) => {
  return class extends Clazz { }
}

const xs = factory(Baz)

@estaub

If A extends B then typeof A is assignable to typeof B

This is true, but if there are some generics, for example:

If A extends B<T> Is typeof A assignable to typeof B?

If typeof B is considered typeof B<any>, then typeof A is assignable to typeof B If typeof B is considered typeof B<T>, then typeof A is not assignable to typeof B

This is all about how strict it should be, I have no idea if this is a bug or not.


@Saviio

typeof is a value-level operator, so that typeof Base is still considered typeof Base<P> instead of typeof Base<{}>, the following cases are passed, see my comment above for more detail.

class Base<P = {}> {
    constructor(public p: P) { }
}

class AnotherBase<P> {
    constructor(public p: P) { }
}

class Baz extends Base {}

const factory = (Clazz: typeof Base) => {
    return class extends Clazz {}
}

const xs = factory(AnotherBase) // passed
class Base<P = {}> {
    constructor(protected p: P) { }
}

class Foo extends Base {}
class Baz extends Base {}

const factory = (Clazz: typeof Foo) => {
    return class extends Clazz {}
}

const xs = factory(Baz) // passed

Reduced reproduction:

interface MyInterface {
  something: number;
}

var doIt = <T extends MyInterface>(d: Partial<T>) => {}
doIt = (d: Partial<MyInterface>) => {}

@mat3e That error actually looks right to me, but my attempted “fix” for it produces an even stranger error, IMHO.

Changing AllTestClasses to:

// classes container
class AllTestClasses {
    array: TestBase<DefinitionBase>[];
}

results in:

Error:(110, 14) TS2345:Argument of type ‘typeof ConcreteTest1’ is not assignable to parameter of type ‘TestBase<DefinitionBase>’. Property ‘definition’ is missing in type ‘typeof ConcreteTest1’. Error:(111, 14) TS2345:Argument of type ‘typeof ConcreteTest2’ is not assignable to parameter of type ‘TestBase<DefinitionBase>’. Property ‘definition’ is missing in type ‘typeof ConcreteTest2’.

I’m having a similar issue. Seems like stricter checks on generics and typeof are not the perfect match.

interface IDefinition { }

class DefinitionBase implements IDefinition { }

class Def1 extends DefinitionBase { }
class Def2 extends DefinitionBase { }

// ---------------------- usage ----------------------
interface ITest<D extends IDefinition> {
    definition: D;
}

abstract class TestBase<DImpl extends DefinitionBase> implements ITest<DImpl> {
    abstract definition: DImpl;
}

class ConcreteTest1 extends TestBase<Def1> {
    definition: Def1;
}
class ConcreteTest2 extends TestBase<Def2> {
    definition: Def2;
}

// classes container
class AllTestClasses<T extends typeof TestBase> {
    array: T[];
}

// ---------------------- fail ----------------------
var t = new AllTestClasses();
t.array.push(ConcreteTest1); // Def1 vs. DImpl error
t.array.push(ConcreteTest2); // Def2 vs. DImpl error

Errors:

test.ts(31,14): error TS2345: Argument of type 'typeof ConcreteTest1' is not assignable to parameter of type 'typeof TestBase'.
  Type 'ConcreteTest1' is not assignable to type 'TestBase<DImpl>'.
    Types of property 'definition' are incompatible.
      Type 'Def1' is not assignable to type 'DImpl'.
test.ts(32,14): error TS2345: Argument of type 'typeof ConcreteTest2' is not assignable to parameter of type 'typeof TestBase'.
  Type 'ConcreteTest2' is not assignable to type 'TestBase<DImpl>'.
    Types of property 'definition' are incompatible.
      Type 'Def2' is not assignable to type 'DImpl'.

The only way to make it work with TS 2.4.1 is to use "noStrictGenericChecks": true option. Hard to apply the workaround from @ikatyang