I see the use case for this being that you want GenericType
s to have any kind of string as a key but still want to determine exactly what those keys' values can be at the point where you declare them.
In that case you can make use of the Record
type to restrict the allowed keys of Obj1
to only the keys you specify.
type GenericType<K extends string> = Record<K, {
prop1: string,
prop2?: string,
prop3?: number,
}>
Then when defining Obj1
you can specify what the allowed keys should be by setting the union of allowed keys as the first type parameter.
const Obj1: GenericType<"key1" | "key2"> = {
key1: {
prop1: "hi",
},
key2: {
prop1: "bye",
prop2: "sup",
},
};
TypeScript will now let you access both key1
and key2
with full type safety.
Obj1.key1
// (property) key1: {
// prop1: string;
// prop2?: string | undefined;
// prop3?: number | undefined;
// }
EDIT
Based on OP's comment it sounds like he would rather not specify all the key names or have to check for the presence of optional fields manually.
The best way I can think of doing this while still ensuring that the object you declare matches the constraints of the GenericType
interface is to do something like the following.
First you need this utility type:
type Constraint<T> = T extends Record<string, {
prop1: string,
prop2?: string,
prop3?: number,
}> ? T : never
This will return never
if T
doesn't match the constraint or just T
otherwise.
Now you declare the plain object you actually want. No type annotations.
const CorrectObj = {
key1: {
prop1: "hi",
},
key2: {
prop1: "bye",
prop2: "sup",
},
};
Then you assign this object literal to another variable, but declaring that the new variable has to be of type Constraint<typeof CorrectObj>
const CheckedObj: Constraint<typeof CorrectObj> = CorrectObj
If CorrectObj
matches the constraint then CheckedObj
will be a simple copy of CorrectObj
with all fields available.
If the object literal doesn't match the constraints however you will get a type error when trying to assign CheckedBadObj to the literal:
const BadObj = {
key1: {
progfdgp1: "hi",
},
key2: {
prop1: "bye",
prdfgop2: "sup",
},
};
const CheckedBadObj: Constraint<typeof BadObj> = BadObj
// ^^^^^^^^^^^^^
// Type '{ key1: { progfdgp1: string; }; key2: { prop1: string; prdfgop2: string; }; }' is not assignable to type 'never'. (2322)
The explanation being that Constraint<T>
when T
doesn't match is never
, but you are still trying to assign a non-never
value to CheckedBadObj
, causing a type conflict!
This involves a bit of duplication in declaring two instances of every object literal but is the only way of
- having TypeScript know exactly which fields exist on the object, including all sub-objects, while still
- checking that the values of your "generic" types
match your constraints
You can play around with this approach in the playground.