Search code examples
typescripttypescript2.8conditional-types

How to capture type argument of nested property and map it to something else


I don't really know how to ask this question so I think the best way is with an example of what I'm trying to do. Let's say I have the following object:

const obj = {
  one: 'some string',
  two: new Set<string>(),
};

and now I want to write a function that takes in that object and converts the Sets to Arrays of the same type.

Here's the untyped javascript implementation:

const obj = {
  one: 'some string',
  two: new Set().add('one').add('two'),
};

function convertToArrays(objWithSets) {
  return Object.entries(objWithSets).reduce((objectWithArrays, [key, value]) => {
    if (value instanceof Set) {
      objectWithArrays[key] = Array.from(value);
    } else {
      objectWithArrays[key] = value;
    }
    return objectWithArrays;
  }, {});
}


console.log('converted', convertToArrays(obj));

How can I properly type the above function? To clarify: it can take in any object (not just the one, two example) and if it sees a Set<T>, it transforms that to an Array<T>.

I know it requires capturing the type of the inner Set (which is string in this case) and conditionally mapping that type to Array<string>.

Thanks!


Solution

  • You can use a mix of mapped types and conditional types to do this:

    type InnerSetToArray<T> = { 
        [P in keyof T]: 
            T[P] extends Set<infer U> 
                ? Array<U> 
                : T[P] 
    };
    
    type InnerSet = { one: number, two: Set<string>, three: Set<Function>, four: Date };
    
    // InnerArray === { one: number, two: string[], three: Function[], four: Date }
    type InnerArray = InnerSetToArray<InnerSet>;
    

    Edit: original, overly specific answer below:

    Here we go, essentially as you describe:

    function convertToArrays<T extends { one: string, two: Set<any> }>(objWithSets: T):
        T extends { one: string, two: Set<infer U> }
            ? { one: string, two: Array<U> }
            : never {
      /* ... */
    }
    
    // typeof x === { one: string, two: number[] }
    const x = convertToArrays({ one: "hello", two: new Set([1, 2, 3]) })
    

    We allow it to initially be Set<any> just to serve as a typeguard, and then infer the actual type with the conditional. The conditional narrows to never on a match failure, which should be fine as the initial check theoretically guarantees the conditional will always evaluate to true.