Map property type only if type is union type

I would like to map the keys of an object type to an object where:

  • Each key is a key from the original type
  • Each value is { enum: Array } if the type of the value is a union type
  • Each value is { enum: never } if the type of the value is not a union type

Example

type OriginalType = {
    foo: "a" | "b";
    bar: "c";
    foobar: Record<string, string>;
};

type MappedType<T> = { ... };

type ResultType = MappedType<OriginalType>;

// ResultType = {
//    foo: { enum: ["a", "b"] }, // or foo: { enum: Array<"a" | "b"> }
//    bar: { enum: never },
//    foobar: { enum: never },
//}

I know how to map the keys and values to a new type, but not how to determine whether a type is a union type:

type IsUnion<T> = ...

type MappedType<T> = {
    [K in keyof T]: IsUnion<T[K]> extends true ? { enum: Array<T[K]> } : never;
};

TypeScript doesn’t directly support distinguishing unions from non-unions, so any user-defined implementation of this might have strange edge cases. One possible approach to detecting a union type is to use a distributive conditional type to split a type into its union members, and to compare each member with the whole unsplit type. If the type is a union, each piece will be different from the whole. Otherwise there will be only one piece and it will be the same as the whole. (Well, or there will be zero pieces if the input is never, but hopefully that’s an edge case I don’t have to worry about.) Like this:

type IsUnion<T, U = T> = 
  T extends unknown ? [U] extends [T] ? false : true : never;

Here I use a default generic type argument to copy T into another type parameter U. We split T into union members (via the distributive conditional type T extends unknown ? ⋯ : never and we keep U unsplit (via the non-distributive conditional type [U] extends [T] ? false : true. Let’s test it out:

type U = IsUnion<number>; // false
type V = IsUnion<string | number>; // true

Looks reasonable. And for your example:

type MappedType<T> = {
  [K in keyof T]: IsUnion<T[K]> extends true ? { enum: Array<T[K]> } : never;
};
type OriginalType = {
  foo: "a" | "b";
  bar: "c";
  foobar: Record<string, string>;
};
type ResultType = MappedType<OriginalType>;
/* type ResultType = {
    foo: {
        enum: ("a" | "b")[];
    };
    bar: never;
    foobar: never;
} */

Looks good.

Playground link to code

Leave a Comment