Reputation: 4258
I have a Generic type
type GenericType = {
[key: string]: {
prop1: string,
prop2?: string,
prop3?: number,
},
};
I use the Generic type to help build / typecheck a new object I created.
const Obj1: GenericType = {
key1: {
prop1: "hi",
},
key2: {
prop1: "bye",
prop2: "sup",
},
};
It works. However, when I use the new object, vscode / typescript doesn't show the keys or props of Obj1
without removing GenericType
from it. I can also "extend" the type but that's code duplication.
Obj1.???
Is there way to keep the GenericType while having access to the more specific props from the new object I created from it?
Update 1
I expect vscode / typescript to show / validate
Obj1.key1.prop1
Obj1.key2.prop1
and error if
Obj1.key1.prop2
Obj1.key2.prop3
Obj1.key2.prop321
Upvotes: 2
Views: 7179
Reputation: 276239
Use a generic utility function (enforce
) that
GenericType
using a generic constraint (extends
)Code:
type GenericType = {
[key: string]: {
prop1: string,
prop2?: string,
prop3?: number,
};
};
const enforce = <T extends GenericType>(obj: T): T => obj;
type GenericType = {
[key: string]: {
prop1: string,
prop2?: string,
prop3?: number,
};
};
const enforce = <T extends GenericType>(obj: T): T => obj;
const Obj1 = enforce({
key1: {
prop1: "hi",
},
key2: {
prop1: "bye",
prop2: "sup",
},
});
Obj1.key1.prop1; // Ok
Obj1.key2.prop1; // Ok
/**
* ERROR: does not match passed in object
*/
Obj1.key1.prop2 // Error
Obj1.key2.prop3 // Error
Obj1.key2.prop321 // Error
Obj1.key3; // Error
/**
* ERRORS: Does not match GenericType
*/
const Obj2 = enforce({
key1: { // Error
}
});
const Obj3 = enforce({
key1: {
prop1: 123, // Error
}
});
Upvotes: 0
Reputation: 9238
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
You can play around with this approach in the playground.
Upvotes: 3
Reputation: 5967
Assuming that your goal is to have key1
and key2
show up in the autocomplete menu for Obj1
but still have the ability to set additional keys later, here is a possible solution:
type GenericType = {
[key: string]: {
prop1: string,
prop2?: string,
prop3?: number,
};
};
const generify = <T extends GenericType>(obj: T): T & GenericType => obj;
const Obj1 = generify({
key1: {
prop1: "hi",
},
key2: {
prop1: "bye",
prop2: "sup",
},
});
I cannot think of a simpler solution right now that would give Obj1
the same intersection type between GenericType
and the specific type containing only the key1
and key2
properties.
Upvotes: 1