0

So, i'm working on some typescript code and i found this weird bug that i don't know why it happens. I'm just starting with the language so it might be a newbie question but anyway, this is really strange to me.

First thing first i have a typescript playground so you guys can iterate on it to help me on solving it

Anyway, so here is the code i'm working on, just an example:

class Model {
  fields!: ModelFields;
  options!: ModelOptions<this>;
}

class ModelWithoutIndex {
  fields!: ModelFields;
  options!: ModelOptionsWithoutIndex<this>;
}

class ModelWithIndexNotInBrackets {
  fields!: ModelFields;
  options!: ModelOptionsNotInBracket<this>;
}

class User extends Model {
  fields = {
    a: FieldTypes.BOOL
  }
//                        V - Why? and how to solve it?
  options: ModelOptions<this> = {
    indexes: [{
      unique: true,
      fields: ['a']
    }]
  }
}

class UserWithoutIndex extends ModelWithoutIndex {
  fields = {
    a: FieldTypes.BOOL
  }
//                                   V - This works, why???? You know what's strange, it just don't work because ModelIndex is an Object defined in {}
  options: ModelOptionsWithoutIndex<this> = {
  }
}

class UserWithIndexNotInBrackets extends ModelWithIndexNotInBrackets {
  fields = {
    a: FieldTypes.BOOL
  }
//                                   V - As i said, why this works?, This doesn't make sense to me
  options: ModelOptionsNotInBracket<this> = {
  }
}

// ------------ TYPES ----------------
enum FieldTypes {
  CHAR = 'CHAR',
  BOOL = 'BOOL',
  INT = 'INT'
}

type ModelIndex<M extends Model> = {
  unique: true
  fields: (keyof M["fields"])[]
}

type ModelIndexesNotInBracket<M extends ModelWithIndexNotInBrackets> = (keyof M["fields"])[]

type OrderingOfModelOptions<M extends Model | ModelWithoutIndex | ModelWithIndexNotInBrackets> = keyof M["fields"]|
  keyof { [F in keyof M["fields"] as F extends string ? `-${F}` : never] : 1}


type ModelOptionsNotInBracket<M extends ModelWithIndexNotInBrackets> = {
  indexes?: ModelIndexesNotInBracket<M>[];
  ordering?: OrderingOfModelOptions<M>[];
}

type ModelOptionsWithoutIndex<M extends ModelWithoutIndex> = {
  ordering?: OrderingOfModelOptions<M>[];
}
type ModelOptions<M extends Model> = {
  indexes?: ModelIndex<M>[];
  ordering?: OrderingOfModelOptions<M>[];
}

type ModelFields = {
  [field: string]: FieldTypes
}

As i was debugging i saw that ModelIndex actually is passed as ModelIndex<M & Model> but this is not what i want, what i want is ModelIndex<M>, i don't know and i don't understand why typescript does this automatic intersection.

Thanks a lot in advance

  • 1
    Can you try to reduce this further to give a [mre]? There's an awful lot of stuff going on in that code, and if you can make it more minimal, it will be easier to investigate. – jcalz Jul 10 '22 at 15:49
  • If your question is really just "why does this happen", it's because, `this` is considered an implicitly generic subtype of the current class (so in `Model`, `this` is going to be some *subtype* of `Model`). And your `ModelIndex` type uses `keyof T['xxx']`, and `keyof` is *contravariant* in its argument, so if `X extends Y` then `keyof Y extends keyof X` and not necessarily vice versa. Generics quickly become *invariant* and cannot be used in subtypes easily if you have complex type functions. – jcalz Jul 10 '22 at 15:54
  • See [this question and answer](https://stackoverflow.com/questions/66410115/difference-between-variance-covariance-contravariance-and-bivariance-in-typesc) for explanations of variance (co-, contra-, and in-). I could write this up as an answer with more of an explanation, but I wonder if you think it would fully address the question. If not, what is missing for you? – jcalz Jul 10 '22 at 15:55
  • Thanks a lot for the answer @jcalz, i've submitted an issue on github and it worked, it was really simple so solve, i submitted an answer here so it can help others with the same problem as me – Nicolas Leal Jul 10 '22 at 16:09

1 Answers1

0

I've submitted an issue and they responded me, types are not automatically asserted on class property initializers. You can see the answer here but i will also translate for others

Your property fields has the type { a: FieldTypes }, but your generic type ModelOptions<> requires the type argument to extend Model. But the type of this is { fields: { a: FieldTypes } }, and it does not extend Model (which is { fields: ModelFields; options: ModelOptions<this> }).

When you type your property fields correctly it works:

class User extends Model {
  fields: ModelFields = {
    a: FieldTypes.BOOL
  }
  // ...
}

The type of re-defined properties is not inherited. This would require #10570.