I want to enforce this with a generic type: A record of objects where the properties of each inner object must all be of the same type. The property types don't have to match the property types of other objects, each inner object only needs to be internally consistent.
I thought this would be pretty easy, but it's turning out to be pretty hard! This is what I came up with at first:
// should make sure that all properties are T
type NestedObject<T> = {
[K: string]: T;
};
// maps through a record of unknown types, returns a type where
// each property is an object of matching types
// (does not work)
type OuterObject<T extends Record<string, unknown>> = {
[K: string]: NestedObject<T[keyof T]>;
};
const validExample: OuterObject<{
pets: string;
friendsAges: number;
}> = {
pets: {
dog: "awesome",
cat: "so great",
dragon: "scary",
},
friendsAges: {
molly: 29,
manuel: 44,
},
};
const invalidExample: OuterObject<{
pets: string;
friendsAges: number;
}> = {
pets: {
dog: "awesome",
cat: "so great",
dragon: "scary",
},
friendsAges: {
molly: 29,
manuel: "fourty-four", // Should have a type error here, but there isn't one
buddy: [40]
// ^ type error: Type 'string[]' is not assignable to type 'string | number'
},
};
I'm guessing it doesn't work because I'm not restricting the type per object: NestedObject<T[keyof T]>
will be the same type for every inner object.
This seems to be the case: typescript doesn't care if I provide a string property to an object that starts with a number property, but it does care if I provide an array[] type, showing the error: Type 'string[]' is not assignable to type 'string | number'
.
Why is the type of each property the union of the types I provided in the generic? How can I tell typescript that OuterObject<{ pets: string; friendsAges: number; }
means
pets
is a stringfriendsAges
is a numberIf you want OuterObject<T>
keep track of the relationship between each key of T
and its corresponding value, then you have to make it a mapped type over T
, of the form {[K in keyof T]: F<T[K]>}
. You can't just give it an index signature like {[k: string]: F<T[keyof T]>}
because T[keyof T]
loses that relationship.
So that means it looks like
type OuterObject<T extends object> = {
[K in keyof T]: NestedObject<T[K]>;
};
And you can verify that it does what you want:
type O = OuterObject<{ pets: string, friendsAges: number }>;
/* type O = {
pets: NestedObject<string>;
friendsAges: NestedObject<number>;
} */