Generic type with any property of certain type in typescript

1.1k views Asked by At

Hi everyone!

interface Thing {
  name: string;
}

interface ThingMap {
  [thingName: string]: Thing;
}

interface ThingMapClassA {
  first: { name: 'first thing name' };
  second: { name: 'second thing name' };
  third: { name: 'third thing name' };
}

interface ThingMapClassB {
  first: { name: 'first thing name' };
  second: { name: 'second thing name' };
  third: { name: 'third thing name' };
}

class Handler<T extends ThingMap> {}

const handler = new Handler<ThingMapClassA>();

I world like Handler to accept any class with properties (ideally at least one) of type Thing. But ThingMapClassA is not recognised. It leads to an error. Any suggestions?

1

There are 1 answers

0
jcalz On BEST ANSWER

The type

interface ThingMap {
  [thingName: string]: Thing;
}

has a string index signature, meaning that if an object of that type has a property whose key is a string, the value of that property will be a Thing.

If you have an anonymous object type, such as the type inferred from an object literal, and try to assign it to a type with an index signature, the compiler will helpfully give it an implicit index signature:

const goodVal: ThingMap = { a: { name: "b" } }; // okay

const badVal: ThingMap = { a: "oops" }; // error
// ----------------------> ~
// Type 'string' is not assignable to type 'Thing'.
// The expected type comes from this index signature.

But implicit index signatures are not given to values of interface or class instance types. This is described at microsoft/TypeScript#15300. Observe:

interface Iface {
  a: Thing;
}
const iface: Iface = { a: { name: "b" } };
const alsoBad: ThingMap = iface; // error!
// Index signature for type 'string' is missing in type 'Test'.

class Cls {
  a = { name: "abc" }
}
const cls: Cls = new Cls();
const alsoAlsoBad: ThingMap = cls; // error!
// Index signature for type 'string' is missing in type 'Cls'.

And that's the problem you're running into. ThingMapClassA and ThingMapClassB are not assignable to ThingMap, even though an anonymous object literal type equivalent to either one would be. So you'll need to change what you're doing.


The easiest approach here is to change your constraint to be recursive. You don't need T to have a string index signature; you just want to know that its properties are assignable to Thing. That can be expressed as

class Handler<T extends Record<keyof T, Thing>> { }

using the Record<K, V> utility type. Record<keyof T, Thing> means "an object with the same keys as T, whose properties are of type Thing". So if T extends Record<keyof T, Thing>, then we know that every property of T is of type String.

So that gives us

const handler = new Handler<ThingMapClassA>(); // okay

and

const badHandler = new Handler<{ a: Thing, b: string }>(); // error!
// --------------------------> ~~~~~~~~~~~~~~~~~~~~~~~
// Types of property 'b' are incompatible.
// Type 'string' is not assignable to type 'Thing'

as desired.

Playground link to code