Home > Software design >  Typescript. How to create generic helper function for following type?
Typescript. How to create generic helper function for following type?

Time:04-18

Sorry for unclear title but I have no clue on how to describe my question shortly.

I have some type with one literal value and object with flat structure of this types as values (simplified version):

type SomeType<T = string> = { value: T };

type SomeTypeMapped = Record<string, SomeType>;

I want to write helper (generic) to tell TS that

const a = helper({
   A: { value: 'literal-1' },
   B: { value: 'literal-2' }
})

has a type of:

const a: {
   A: SomeType<"literal-1">;
   B: SomeType<"literal-2">;
}

So far the closest solution I found is

function helper<V extends string, K extends string>(val: { [key in K]: SomeType<V> }) {
  return val;
}

const a = helper({
  A: { value: 'asd' }.
  B: { value: 'qwe' }
});

// -->

const a: {
    A: SomeType<"asd" | "qwe">;
    B: SomeType<"asd" | "qwe">;
}

If I try to modify helper as so:

function helper<
  V extends string,
  K extends string,
  T extends { [key in K]: SomeType<V> }
>(val: T) {
  return val;
}

it converts the whole map to constant literal type:

const a: {
    A: {
        value: "asd";
    };
    B: {
        value: "qwe";
    };
}

which is also close to what I'm trying to achieve, but my second point is readability, as

const a: {
    A: SomeType<"asd">;
    B: SomeType<"qwe">;
}

is much more readable for more complicated examples (with more fields than single 'value').

Can I somehow tell TS that field A has exactly type of SomeType<"asd"> as well as field B has type of SomeType<"qwe">?

CodePudding user response:

Here is how I'd write helper():

const helper = 
  <T extends Record<keyof T, string>>(val: { [K in keyof T]: SomeType<T[K]> }) => val;

The generic type parameter T is a mapping from the keys of val to the associated string literal types you want to pass into SomeType. So the intent is that when you call

const a = helper({
    A: { value: 'literal-1' },
    B: { value: 'literal-2' }
})

T will be inferred as { A: "literal-1"; B: "literal-2" }. Then val is not of type T directly, but of the mapped type {[K in keyof T]: SomeType<T[K]>}.

Note that the compiler is indeed able to infer the type T from a value of a homomorphic mapped type of the form {[K in keyof T]: ...T[K]...}. It's not really well-documented in the current version of the TS Handbook, but there is a section of the now-deprecated v1 of the Handbook about inference from mapped types that describes this.

Anyway, that means when you call helper() above, T is inferred properly, and val is inferred to be of this type:

/* const a: {
    A: SomeType<"literal-1">;
    B: SomeType<"literal-2">;
} */

as desired.

Playground link to code

  • Related