I want to create a conditional generic type/interface for an object in TypeScript. Let´s say I have the following interface:
interface CarType{
name: string;
price: number;
}
// Which will allow this Object
const ferrari: CarType = {
name: "Ferrari F50",
price: 300000,
}
I want to create an interface or type that allows the following.
Note: These are examples. To make it clear what the PartOfObjectType should achieve
// Should compile
const partOfFerrari: PartOfObjectType<CarType> = {
prop: "price",
value: 300000 // All numbers should work because the property price has the type number
}
// Should not compile
const partOfFerrari: PartOfObjectType<CarType> ={
prop: "price",
value: false // Should not work because property price has the type number
}
// Should compile
const partOfFerrari: PartOfObjectType<CarType> ={
prop: "name",
value: "Banana" // All strings should work because the property name has the type string
}
// Should not compile
const partOfFerrari: PartOfObjectType<CarType> ={
prop: "name",
value: 342123 // Should not work because property name has the type string
}
// Should not compile
const partOfFerrari: PartOfObjectType<CarType> ={
prop: "color", // Color does not exist in CarType
value: 342123
}
// Should not compile
const partOfFerrari: PartOfObjectType<CarType> ={
prop: "name",
// value is missing
}
// Should not compile
const partOfFerrari: PartOfObjectType<CarType> ={
value: "Ferrari F50",
// prop is missing
}
I have tried the following. Type suggestions for the prop are working. But I just cannot get the type suggestions for value to work.
interface PartOfObjectType<ObjectType> {
prop: keyof ObjectType;
value: ObjectType[prop];
}
// I´ve also tried
interface PartOfObjectType<ObjectType> {
prop: keyof ObjectType;
value: ObjectType[keyof ObjectType];
}
I´ve read all about generics and conditionals but just can´t find anything that makes it work with objects.
I´m thankful for any help
You can define PartOfObjectType
as follows:
type PartOfObjectType<T extends object> =
{ [K in keyof T]-?: { prop: K, value: T[K] } }[keyof T];
This distributive object type (using the term coined in microsoft/TypeScript#47109) is created by mapping over the properties of T
to form a new object type with the same keys and whose values are the desired {prop, value}
pairs, and then immediately indexing into it with the keys of T
to produce a union of its property value types.
(In case it matters, the mapped type uses the -?
mapping modifier to make sure that any optional properties in T
don't be come "optional" in the output, otherwise you'll allow undefined
to be a PartOfObjectType<T>
. For the example as shown this is unnecessary.)
Let's test it out:
interface CarType {
name: string;
price: number;
}
type CarPart = PartOfObjectType<CarType>
/* type CarPart = {
prop: "name";
value: string;
} | {
prop: "price";
value: number;
} */
You can see that CarPart
has one union member for each property of CarType
, with the desired structure. And that gives you the desired results for your test code:
const partOfFerrari: PartOfObjectType<CarType> = {
prop: "price",
value: 300000
} // okay
const partOfFerrari2: PartOfObjectType<CarType> = {
prop: "price",
value: false // error
}
const partOfFerrari3: PartOfObjectType<CarType> = {
prop: "name",
value: "Banana"
} // okay
const partOfFerrari4: PartOfObjectType<CarType> = { // error
prop: "name",
value: 342123
}
const partOfFerrari5: PartOfObjectType<CarType> = {
prop: "color", // error
value: 342123
}
const partOfFerrari6: PartOfObjectType<CarType> = { // error
prop: "name",
}
const partOfFerrari7: PartOfObjectType<CarType> = { // error
value: "Ferrari F50",
}