Idan Rokach
Idan Rokach

Reputation: 108

Type U (generic) is not assignable to type unknown

I have a generic Repository class and each Repository has its own Sub-Repositories. The Sub-Repositories and kept by a Record {String: Repository}. It looks like this:

export class Repository<T> {
  private readonly collection: Record<string, T> = {};
  private readonly subrepositories: Record<string, Repository<unknown>> = {};
.
.
  subrepository<U>(docName: string) {
    if (!this.subrepositories[docName]) {
      this.subrepositories[docName] = new Repository<U>();
    }
    return this.subrepositories[docName];
  }


In this line: this.subrepositories[docName] = new Repository<U>(); I get typescript error:

TS2322: Type 'Repository<U>' is not assignable to type 'Repository<unknown>'.
Types of property 'set' are incompatible.
Type '(id: string, data: U) => void' is not assignable to type '(id: string, data: unknown) => void'.
Types of parameters 'data' and 'data' are incompatible.
Type 'unknown' is not assignable to type 'U'.
'U' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.

The only way I managed to overcome the issue is by casting this.subrepositories first to unknown and then to Record<string, Repository<U> like that:

  subrepository<U>(docName: string) {
    if (!this.subrepositories[docName]) {
      (this.subrepositories as unknown as Record<string, Repository<U>>)[
        docName
      ] = new Repository<U>();
    }
    return this.subrepositories[docName];
  }

But is it really a solution?

Upvotes: 1

Views: 2191

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249696

Your type is probably invariant on the type parameter T. This happens if you use a type parameter in both a covariant position (such as a field) and a contravariant position (such a as a function parameter). You can learn more about variance here

export class Repository<T> {
  private readonly collection: Record<string, T> = {};
  private readonly subrepositories: Record<string, Repository<unknown>> = {};
  fn!: (p: T) => void; // Remove this and asssignability works
  subrepository<U>(docName: string) {
    if (!this.subrepositories[docName]) {
      this.subrepositories[docName] = new Repository<U>();
    }
    return this.subrepositories[docName];
  }
}

Playground Link

The reason this doesn't work is that it is actually not type safe. Since you could pass in a type that wasn't expected by the function.

  sampleMethod() {
      const rep = this.subrepository<{ a: number} >("foo");
      rep.fn =  p => p.a.toFixed();

      
      const repAlias = this.subrepository<{ a: string} >("foo");
      repAlias.fn({ a: "" }) // 💥
  }

Playground Link

Also you should avoid using type parameters in a single position, as they don't provide a lot of type safety, usually just masking type assertions

Upvotes: 2

Related Questions