Reputation: 489
I have a generic interface in which I want one property to only include keys from another property, and have the same type as well....
interface IMyInterface<T> {
propA: T;
propB: { [key in keyof T]?: T[key] };
}
This is fine, I've done similar things many times before.... Basically I want it to infer what T is based on what I pass to propA.
However here is my issue, I have a dictionary with many IMyInterface types....
interface IMyDictionary {
[key: string]: IMyInterface;
}
Here is where my problem, the above definition demands a generic type.... but whatever type I use would be applied to every property. If I use any than it defeats the point, because when declaring my dictionary it assumes everything is any.
interface IInterfaceA {
name: string;
age: number;
}
const objA: IInterfaceA = {
name: 'John',
age: 18
}
const myDictionary: IMyDictionary {
test1: {
propA: objA,
propB: { age: 40 }
}
}
So Basically, when assigning the value to myDictionary above, I want it to infer that propA is of type IInterfaceA, and in propB only allow properties name with a type of string and age with a type of number.
I could easily do this by not defining type IMyDictionary, and creating each item one by one, and then assigning them to a new object (myDictionary)....
const test1: IMyInterface = {
propA: objA,
propB: { age: 40 }
}
const myDictionary = { test1 }
This would infer the generic type for test1, and then infer the type for myDictionary, using exact property names, instead of an array of strings. But I would rather use plain old object assignment syntax if possible.
I can't figure out a way to declare the type of IMyDictionary to force it to infer the generic type for each instance of IMyInterface. Alternatively, I'm open to ideas of different ways to define IMyInterface to infer the properties and types for propB.
Thanks in advance!
Upvotes: 3
Views: 1145
Reputation: 329903
The type you're looking for is a dictionary where each entry is some IMyInterface<T>
type, but not any particular T
. This is probably best expressed as an existential type like (possibly) {[k: string]: IMyInterface<exists T>}
, which isn't currently supported natively in TypeScript (nor most other languages with generics). TypeScript (and most other languages with generics) only has universal types: someone who wants a value of type X<T>
can specify any type for T
that they want, and the provider of the value must be able to comply. An existential type is the opposite: someone who wants to provide a value of a type like X<exists T>
can choose any specific type for T
that they want, and the receiver of that value just has to comply. Existential types let you "hide" the generic type inside a non-generic type.
But, TypeScript doesn't have existential types, so we'll have to do something else. (Well, it doesn't have native existential types. You can emulate them by using generic functions and inverting control via callbacks, but that's even more complicated to use than the solution I'm going to suggest next. If you're still interested in existentials you can read the linked article about it)
The next best thing we can do is to describe the shape of IMyDictionary
as a generic type, where we don't really care about it, and get the compiler to infer it for us as much as possible. Here's one way:
type IMyDictionary<T> = { [K in keyof T]: IMyInterface<T[K]> }
const asIMyDictionary = <T>(d: IMyDictionary<T>) => d;
The idea is that instead of declaring a variable to be of type IMyDictionary
, you use the helper function asIMyDictionary()
to take the value and produce a suitable type IMyDictionary<T>
. This only works, by the way, because the type IMyDictionary<T>
is a homomorphic mapped type, so T
can be inferred from a value of type IMyDictionary<T>
. Let's see it in action:
const myDictionary = asIMyDictionary({
test1: {
propA: { name: "John", age: 18 },
propB: { age: 40 }
},
test2: {
propA: { size: 10, color: "blue" },
propB: { color: "purple" }
},
test3: {
propA: { problem: true },
propB: { oops: false } // error!
// ~~~~~~~~~~~~
// 'oops' does not exist in type '{ problem?: boolean | undefined; }'
}
});
This works and gives you errors where you expect them. Yay!
The caveat here is you still have to drag around this T
type you don't care about. Any function or type which you were hoping would just deal with a concrete IMyDictionary
will have to become generic. Maybe that's not too painful to you. If it is, you can think about the "emulated" existential type. But I've already written a lot here, so hopefully the above will help you... if you need me to write out the emulated existential version, I can.
Good luck!
Upvotes: 2