4

I want to create generic method that gets one of the predefined strings as first parameter and some data as second parameter. and I want 'data' to be described as an interface with keys of those predefined strings

type MyKeys = 'lorem' | 'ipsum' | 'dolor';

interface DataParams extends Record<MyKeys, any | undefined> {
    lorem: { year: number };
    ipsum: { to: string };
}

myCoolGeneric('lorem', {year: 2021}); // ok
myCoolGeneric('ipsum', {year: 2021}); // typescript error

so I can specify several sets of keys and different available params for them. And typescript will check that method is called with correct params depending on which key is used as argument

How should I declare myCoolGeneric? Is it possible at all? Or maybe there are some other ways to implement such behavior?

I tried following code but it gives error that key is used as value in type

const myCoolGeneric = <P extends Record<string, any | undefined>>(key: keyof P, data P[key]) => { ... }
Anton Karpov
  • 700
  • 7
  • 11

1 Answers1

4

Thanks @kaya3!

UPDATED

If you have different combination of allowed arguments, you can produce function overloads.

type MyKeys = 'lorem' | 'ipsum' | 'dolor';

interface DataParams {
  lorem: { year: number };
  ipsum: { to: string };
}

// credits goes to https://stackoverflow.com/a/50375286
// function intersection produces - function overloads
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

type Values<T> = T[keyof T]

/**
 * Generate all possible combinations of allowed arguments
 */
type AllOverloads = {
  [Prop in MyKeys]:
  Prop extends keyof DataParams
  ? (key: Prop, data: DataParams[Prop]) => any
  : (key: Prop) => any
}

/**
 * Convert all allowed combinations to function overload
 */
type Overloading = UnionToIntersection<Values<AllOverloads>>

const myCoolGeneric: Overloading = <Key extends MyKeys>(
  key: Key,
  data?: Key extends keyof DataParams ? DataParams[Key] : void
) => null as any

myCoolGeneric('lorem', { year: 2021 }); // ok
myCoolGeneric('ipsum', { to: '2021' }); // typescript error
myCoolGeneric('dolor'); // ok

Playground

In TypeScript you can use square bracket notation just like in JS:DataParams[Key]

UPDATE with generic parameters

type MyKeys = 'lorem' | 'ipsum' | 'dolor';

interface DataParams {
  lorem: { year: number };
  ipsum: { to: string };
}

// credits goes to https://stackoverflow.com/a/50375286
// function intersection producec - functin overloads
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

type Values<T> = T[keyof T]

/**
 * Generate all possible combinations of allowed arguments
 */
type AllOverloads<Keys extends string, Mappings> = {
  [Prop in Keys]:
  Prop extends keyof Mappings
  ? (key: Prop, data: Mappings[Prop]) => any
  : (key: Prop) => any
}

/**
 * Convert all allowed combinations to function overload
 */
type Overloading<Keys extends string, Mappings> =
  UnionToIntersection<Values<AllOverloads<Keys, Mappings>>>

const myCoolGeneric: Overloading<MyKeys, DataParams> = (
  key: string,
  data?: unknown
) => null as any

myCoolGeneric('lorem', { year: 2021 }); // ok
myCoolGeneric('ipsum', { to: '2021' }); // typescript error
myCoolGeneric('dolor'); // ok

UPDATE 3

type MyKeys = 'lorem' | 'ipsum' | 'dolor';

interface DataParams {
    lorem: { year: number };
    ipsum: { to: string };
}


// credits goes to https://stackoverflow.com/a/50375286
// function intersection producec - functin overloads
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;
type IsNever<T> = [T] extends [UnionToIntersection<T>] ? true : false;

type Values<T> = T[keyof T]

/**
 * Generate all possible combinations of allowed arguments
 */
type AllOverloads<Mappings, Keys extends string> = {
    [Prop in Keys]:
    Prop extends keyof Mappings
    ? (key: Prop, data: Mappings[Prop]) => any
    : (key: Prop) => any
}

/**
 * Convert all allowed combinations to function overload
 */
type Overloading<Mappings, Keys extends string> =
    keyof Mappings extends Keys
    ? UnionToIntersection<Values<AllOverloads<Mappings, Keys>>>
    : never


const myCoolGeneric: Overloading<DataParams, MyKeys> = (
    key: string,
    data?: unknown
) => null as any

myCoolGeneric('lorem', { year: 2021 }); // ok
myCoolGeneric('ipsum', { to: '2021' }); // typescript error
myCoolGeneric('dolor'); // ok

Try to add to DataParams non existence key. TS will throw an error