Búsqueda de sitios web

typescript - ¿Cómo inferir argumentos basándose en el argumento que se proporcionó a la primera función?


Estoy tratando de crear una función utilitaria que tome un único valor de un enum y que infiera los argumentos de la función devuelta basándose en una interfaz con un campo type que coincida con el valor.

Estoy teniendo dificultades para mapear value y encontrar la interfaz con un campo type que coincida con el value

enum ActionTypes {
  One = 'One',
  Two = 'Two',
  Three = 'Three'
}

interface Action1 {
  type: ActionTypes.One
}

interface Action2 {
  type: ActionTypes.Two
  meta: {
    fieldName: '2'
  }
}

interface Action3 {
  type: ActionTypes.Three
  payload: number[]
  meta: {
    fieldName: '3'
  }
}

type AllActions =
  | Action1
  | Action2
  | Action3

const createAction = <T>(type: T) => (
  payload?: //infer payload from AllActions.type === T,
  meta?: //infer meta from AllActions.type === T,
  ) => ({
  type,
  ...payload && { payload },
  ...meta && { meta }
})

const action1 = createAction(ActionTypes.One)
action1()

const action2 = createAction(ActionTypes.Two)
action2(undefined, {
  meta: {
    fieldName: '2'
  }
})

const action3 = createAction(ActionTypes.Three)
action3([1, 2, 3], {
  meta: {
    fieldName: '3'
  }
})

Campo de Juegos TS

Respuesta #1

Puedes escribir la función, pero hay varias cosas que necesitamos considerar:

  1. T debería extender AllActions['type'] para capturar el tipo literal del valor pasado.

  2. Para extraer la acción asociada con type: T necesitamos usar el tipo condicional predefinido Extract<AllActions, { type: T }>

  3. Dado que según T la función interna puede tener un número variable de parámetros, necesitaremos usar una tupla en los parámetros rest para poder expandir un número variable de parámetros en la función resultante.

  4. Usar tuplas en el parámetro rest con tuplas declaradas manualmente resultará en la pérdida de nombres de parámetros, por lo que en lugar de construir una tupla como [P, M] y expandirla, construiremos en su lugar una firma de función, extraeremos los parámetros como una tupla y expandiremos esa tupla nuevamente en un parámetro rest (function fn(...a: Parameters<(x: number) => void>) {} tendrá un único parámetro numérico llamado x, mientras que function fn(...a:[number]) {} tendrá un único parámetro numérico llamado a_0, lo cual no es ideal)

  5. Podemos usar un tipo condicional personalizado para construir la firma deseada basada en la presencia de payload y meta.

  6. Dado que los tipos condicionales que contienen parámetros de tipo no resueltos son bastante problemáticos al escribir la implementación, usaremos una firma de implementación separada para la función interna.

Poniendo todas estas cosas juntas obtenemos:

type Payload<T> = T extends { payload: infer P } ? P : undefined;
type Metadata<T> = T extends { meta: infer M } ? { meta: M } : undefined;
type CreateActionParameteres<A> =  /*4*/ Parameters<
  /*5*/ A extends { payload: infer P, meta: infer M } ? /*4*/ (payload: P, meta: { meta: M }) => void :
  /*5*/ A extends { payload: infer P } ? /*4*/ (payload: P) => void :
  /*5*/ A extends { meta: infer M } ? /*4*/ (payload: undefined, meta: { meta: M }) => void :
  () => void>

const createAction = <T extends AllActions['type'] /*1*/ >(type: T) =>
{
  function inner(...a: CreateActionParameteres</*2*/ Extract<AllActions, { type: T }>>): /*2*/ Extract<AllActions, { type: T }>
  function inner(payload?: Payload<AllActions>, meta?: Metadata<AllActions>) { /*6*/
    return {
      type,
      ...payload && { payload },
      ...meta && { meta }
    };
  }
  return inner;
}

Enlace del parque