Rico Kahler
Rico Kahler

Reputation: 19202

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!

Upvotes: 0

Views: 129

Answers (1)

y2bd
y2bd

Reputation: 6456

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.

Upvotes: 1

Related Questions