The concept behind this is to allow GenericType
s to have any string as a key while still enforcing specific value types for those keys at the declaration point.
To achieve this, the Record
type can be used to limit the allowed keys of Obj1
to only the specified ones.
type GenericType<K extends string> = Record<K, {
prop1: string,
prop2?: string,
prop3?: number,
}>
When defining Obj1
, you can define the allowed keys by setting a union of keys as the first type parameter.
const Obj1: GenericType<"key1" | "key2"> = {
key1: {
prop1: "hi",
},
key2: {
prop1: "bye",
prop2: "sup",
},
};
This approach allows TypeScript to provide full type safety when accessing both key1
and key2
.
Obj1.key1
// (property) key1: {
// prop1: string;
// prop2?: string | undefined;
// prop3?: number | undefined;
// }
EDIT
Following the OP's preference, rather than specifying all key names or checking optional fields manually, here is an alternative method that ensures the declared object conforms to the constraints of the GenericType
interface.
Firstly, a utility type is needed:
type Constraint<T> = T extends Record<string, {
prop1: string,
prop2?: string,
prop3?: number,
}> ? T : never
It will return `never` if `T` does not meet the constraint, otherwise it will return `T` itself.
Next, declare the plain object without type annotations:
const CorrectObj = {
key1: {
prop1: "hi",
},
key2: {
prop1: "bye",
prop2: "sup",
},
};
Then assign this object literal to another variable, ensuring the new variable is of type
Constraint<typeof CorrectObj>
const CheckedObj: Constraint<typeof CorrectObj> = CorrectObj
If `CorrectObj` fits the constraint, `CheckedObj` will just be a copy with all fields accessible. However, if the literal does not match the constraints, assigning `CheckedBadObj` to it will result in a type error:
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 reason being that when `Constraint<T>` fails, it returns `never`, causing a conflict when trying to assign a non-never value to `CheckedBadObj`.
Although there is some redundancy in declaring two instances of each object literal, this method is necessary for having precise knowledge of the object's fields, including nested objects, while verifying their values against set constraints.
Feel free to experiment with this technique in the playground.