Search code examples
typescriptmapped-types

How to replace properties using mapped types in Typescript


I have a Factory function that receives an object and if that object has properties with a certain name, the factory converts those properties into methods.

How can I use mapped Types to correctly represent the type of the output object?

For example, let's say the convertible properties are foo, bar, baz:

interface IFactoryConfig {
   foo?: string;
   bar?: string;
   baz?: string;
}

And the replacement properties are:

interface IFactoryResult {
   foo(someParam: string): boolean;
   bar(): number;
   baz(otherParam: number): void;
}

If the input's type is

interface IInputObject {
   baz: string;
   notPredefined: string;
   aNumber: number;
   foo: string;
   aMethod(): void;
}

The factory replaces baz and foo with methods and returns:

interface IInputObject {
   baz(otherParam: number): void;
   notPredefined: string;
   aNumber: number;
   foo(someParam: string): boolean;
   aMethod(): void;
}

I'm trying to use mapped types to replace the properties:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

interface IFactory {
   <InputType extends IFactoryConfig, ResultType>(config: InputType): Omit<InputType, keyof IFactoryConfig> & Pick<IFactoryResult, ?>;
}

I don't know what to put in the Pick<> to pick from IFactoryResult the properties that also appear on InputType.


Solution

  • We're just talking about the type-level stuff here, not run-time behavior. You can use conditional types inside your mapped type to perform the check. Here's a general property replacer:

    type ReplaceProps<T, From, To> = { [K in keyof T]:
      K extends keyof From ? T[K] extends From[K] ? K extends keyof To ? To[K] 
      : T[K] : T[K] : T[K]
    }
    

    The idea is that any property in T whose key and value type is also found in From and whose key is found in To will be replaced by the property type in To; otherwise it leaves the property alone.

    Then you can use it like this:

    type IInputObjectOut = ReplaceProps<IInputObject, IFactoryConfig, IFactoryResult>;
    

    and inspecting IInputObjectOut you can see it matches your desired type:

    type IInputObjectOut = {
      baz: (otherParam: number) => void;
      notPredefined: string;
      aNumber: number;
      foo: (someParam: string) => boolean;
      aMethod: () => void;
    }    
    

    I think you could define your IFactory type like this, assuming it's supposed to be callable and behaves like ReplaceProps for its input type:

    interface IFactory {
      <T>(config: T): ReplaceProps<T, IFactoryConfig, IFactoryResult>;
    }
    
    declare const iFact: IFactory;
    declare const input: IInputObject;
    input.foo; // string
    input.aNumber; // number
    const output = iFact(input); // ReplaceProps<IInputObject, IFactoryConfig, IFactoryResult>;
    output.foo("hey"); // boolean
    output.aNumber; // number
    

    Does that work for you? Hope it helps. Good luck!