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? 😊
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.