Search code examples
typescriptgenericsconditional-statementstypescript-generics

TypeScript Conditional Generic Type with Object Properties and Values


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


Solution

  • 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",
    }
    

    Playground link to code