Typescript Map of Arbitrary Generics

typescript generics
typescript map type
typescript keyof
typescript infer generic type
typescript generic map
typescript either
typescript generic arrow function
typescript map of maps

I'm trying to define two types, which should look something like:

export type IQuery<P, U>  = {
  request: string;
  params: (props: P, upsteam?: U) => object;
  key: (props: P, upstream?: U) => string;
  forceRequest: boolean;
  depends?: QueryMap
}

export type QueryMap = {
  [k: string]: IQuery
};

The constraints I'm trying to express are that params and key have the same types for their two arguments, and that a QueryMap is just a mapping from a string to an arbitrary IQuery (doesn't matter what the types are). The compiler complains here because it wants a type to be specified for IQuery, but the point is that each IQuery in the map should be independently parameterized. Is there any way to express this in typescript?

Additionally, if possible, I'd like to get information/guarantees about the shape of the upstream QueryMaps present in the IQuery as I iterated through this tree.

The simplest thing you can do is this:

export type QueryMap = {
  [k: string]: IQuery<any, any>
};

It's not completely type-safe, but it is not too far off what you're trying to represent. If you don't want to lose type information for a value of type QueryMap, allow the compiler to infer a narrower type and use a generic helper function to ensure it is a valid QueryMap, like this:

const asQueryMap = <T extends QueryMap>(t: T) => t;

const queryMap = asQueryMap({
  foo: {
    request: "a",
    params(p: string, u?: number) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
});

The value queryMap.foo.params is still known to be a method that accepts a string and an optional number, even though the type QueryMap['foo']['params'] isn't.

If you specify something not assignable to a QueryMap you will get an error:

const bad = asQueryMap({
  foo: {
    request: "a",
    params(p: string, u?: number) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  },
  bar: {
    request: 123,
    params(p: number, u?: string) {return {}},
    key(p: number, u?: string) {return "nope"},
    forceRequest: false
  }
}); // error! bar.request is a number

The not-completely type-safe problem is shown here:

const notExactlySafe = asQueryMap({
  baz: {
    request: "a",
    params(p: number, u?: string) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
});

This is accepted, even though there's no consistent reasonable values of P and U that works here (which is what happens when you use any). If you need to lock this down more, you can try to have TypeScript infer sets of P and U values from the value or warn you if it cannot, but it's not staightforward.

For completeness, here's how I'd do it... use conditional types to infer P and U for each element of your QueryMap by inspecting the params method, and then verify that the key method matches it.

const asSaferQueryMap = <T extends QueryMap>(
  t: T & { [K in keyof T]:
    T[K]['params'] extends (p: infer P, u?: infer U) => any ? (
      T[K] extends IQuery<P, U> ? T[K] : IQuery<P, U>
    ) : never
  }
): T => t;

Now the following will still work:

const queryMap = asSaferQueryMap({
  foo: {
    request: "a",
    params(p: string, u?: number) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
});

while this will now be an error:

const notExactlySafe = asSaferQueryMap({
  baz: {
    request: "a",
    params(p: number, u?: string) { return {} },
    key(p: string, u?: number) { return "hey" },
    forceRequest: true
  }
}); // error, string is not assignable to number

This increases your type safety marginally at the expense of a fairly complicated bit of type juggling in the type of asSaferQueryMap(), so I don't know that it's worth it. IQuery<any, any> is probably good enough for most purposes.


Okay, hope that helps; good luck!

Using Type Argument Inference When Accepting Generic Callbacks , Argument Inference in TypeScript when accepting generic callbacks. callback could return any arbitrary Type; which means that the .map()� There are no variadic kinds in TypeScript, but you can probably use tuple types instead like CommonClass<T, R extends any[]> with CommonClass<Obj1,[Obj2, Obj3]> and CommonClass<Obj4, [Obj5]> being instances; a minimal reproducible example would be helpful though since you're not doing anything with T and R for me to show how it would work with

You could use IQuery<any, any>.

I'm not sure what you're hoping for in the second part of the question. TypeScript doesn't give you runtime type information. If you just want to have type variables to refer to as you manipulate a single IQuery, you can pass an IQuery<any, any> to a function myFunction<P, U>(iquery: IQuery<P, U>) { ... }.

Typescript, Map with generic keys (part 1) | Toni Petrina, While great in most cases, the default implementation doesn't work with arbitrary keys - it only works with numbers and strings (and objects via� To solve this, TypeScript introduced generics. Generics uses the type variable <T>, a special kind of variable that denotes types. The type variable remembers the type that the user provides and works with that particular type only. This is called preserving the type information. The above function can be rewritten as a generic function as below.

The Solution

I removed from your types unrelevant information just for clarity. The solution boils-down to basically add 3 lines of code.

type Check<T> = QueryMap<T extends QueryMap<infer U> ? U : never>

export type IQuery<P, U, TQueryMap extends Check<TQueryMap>> = {
    prop1: (param1: P, param2?: U) => number;
    prop2: (param1: P, param2?: U) => string;
    prop3?: TQueryMap
}

export type QueryMap<T> = {
  [K in keyof T]: T[K]
};

// type constructors
const asQueryMap = <T>(x: QueryMap<T>) => x
const asQuery = <P, U, V extends QueryMap<any>>(x: IQuery<P, U, V>) => x

Considerations

All types are correctly infered by the compiler.

Important: If (and only if) you use the type constructors (see above) to construct yours structures you can consider yourself totally statically type-safe.

Bellow are the test cases:

Test of no compile errors
// Ok -- No compile-time error and correctly infered !

const queryMap = asQueryMap({
    a: asQuery({
        prop1: (param1: string, param2?: number) => 10,
        prop2: (param1: string, param2?: number) => "hello",
    }),

    b: asQuery({
        prop1: (param1: string, param2?: string) => 10,
        prop2: (param1: string, param2?: string) => "hello",
    }),

    c: asQuery({
        prop1: (param1: Array<number>, param2?: number) => 10,
        prop2: (param1: Array<number>, param2?: number) => "hello",
    })
})


const query = asQuery({
    prop1: (param1: string, param2?: number) => 10,
    prop2: (param1: string, param2?: number) => "hello",
    prop3: queryMap    
})
Test of Compile-time errors

You can see bellow some compile-time errors beeing catched.

// Ok --> Compile Error: 'prop2' signature is wrong

const queryMap2 = asQueryMap({
    a: asQuery({
        prop1: (param1: Array<string>, param2?: number) => 10,
        prop2: (param1: Array<number>, param2?: number) => "hello",
    })
})


// Ok --> Compile Error: 'prop3' is not of type QueryMap<any>

const query2 = asQuery({
    prop1: (param1: string, param2?: number) => 10,
    prop2: (param1: string, param2?: number) => "hello",
    prop3: 10 // <---- Error !
})

Thank you Cheers

Handbook - Generics, While using any is certainly generic in that it will cause the function to accept any and all types for the type of arg , we actually are losing the information about what � Supported Typescript constructs: - interfaces/object literals (no extends, but all the other goodies are there, such as index signatures and optional properties) - intersection types - union types - generics - literals - arrays/maps/sets - tuples - type aliases - circular types/circular references

An intro to TypeScript generics, Abstract classes 6. enums We know that TypeScript has built-in types and you can A generic type can be represented by an arbitrary letter(s), e.g. T in In a map, developers often use the letter K for key and V for value. Why You Should Consider Using TypeScript Generics Instead of Any. A quick overview of a powerful feature all we do is define an arbitrary type as R and then tell the function that our return

Generic enumerated type parameter narrowing (conditional types , [X] This wouldn't be a breaking change in existing TypeScript / JavaScript code those lack the ability to handle arbitrary subsets of the first union and map them to Generic type inference prefers conditional types #31766. Doesn't look like TypeScript is respecting the spec for map iteration, at least according to MDN which specifies a for-of loop. .forEach is sub-optimal, since you can't break it , AFAIK – Samjones Jan 27 '17 at 20:36

Exploiting Generics in TypeScript -- Visual Studio Magazine, Using generics in TypeScript is very similar to using them in, interface because you can apply interfaces to any arbitrary collection of classes. Looks like you want something that allows a variable number of parameters with arbitrary types, but then captures them so they can be repeated in another signature. I think you may find a relevant discussion at #3622 (comment) and #3870.

Comments
  • That helps quite a bit. The thing about conditional types and using the infer keyword is more or less what I was looking for. I should say that these maps are going to be predefined (They are the queries used throughout the app), so I am okay with making them types themselves; I just wanted the linter/compiler to yell when I make a malformed query. I also want to make some guarantees about the shape of certain function returns, i.e. getResponse(QueryMap) would have a return shape equal to all the keys in the map and upstream maps. This is great, thank you!