Búsqueda de sitios web

typescript - ¿Cómo puedo indicar a TS que cada propiedad en un argumento debería corresponder a una propiedad en el tipo de retorno?


Estoy tratando de escribir una función main que tome un objeto opts y devuelva otro objeto. opts tiene dos propiedades opcionales a y b.

Si a está presente, el objeto devuelto debería tener una propiedad aResult. Si b está presente, el objeto devuelto debería tener una propiedad bResult. Si tanto a como b están presentes en opts, entonces tanto aResult como bResult deberían estar presentes en el objeto devuelto.

Puedo lograr esto con sobrecargas:

interface usesA {
    a: string;
}

interface usesB {
    b: string;
}

interface aReturnType {
    aResult: string;
}

interface bReturnType {
    bResult: string;
}

function main(opts: usesA): aReturnType;
function main(opts: usesB): bReturnType
function main(opts: usesA & usesB): aReturnType & bReturnType; 
// implementation omitted

Pero esto se vuelve bastante largo si quiero agregar más propiedades opcionales: c que se mapea en cResult, d que se mapea en dResult, etc.

¿Hay una manera más concisa de hacer esto, por ejemplo, usando una función genérica?

Respuesta #1

Dada una interfaz como:

// Mapping of argument types to return types
interface MyMapping {
  a: { aResult: string }
  b: { bResult: string }
}

Definitivamente puedes hacer algo de magia.

Primero querrás un tipo para tu argumento. Este será cada clave del mapeo opcionalmente incluida, con un valor de string.

// Type of arguments. Each key in the mapping has a string value.
type UsesArgs<T> = { [K in keyof T]?: string }

Obtener el tipo de retorno es un poco más complicado. Básicamente, quieres mapear sobre cada propiedad del argumento, y luego buscar ese tipo en el mapeo. Ten en cuenta que al iterar necesitamos asegurarnos de que las claves sean del tipo keyof Args & keyof Mapping ya que queremos estar seguros de que cada clave esté presente en ambos tipos.

// Type of each possible return value, as a union of all types.
type ReturnValueTypeUnion<Args, Mapping> = {
  // for each key Args and Mapping have in common, get the type from mapping
  [K in keyof Args & keyof Mapping]: Mapping[K]

// Get values from mapping as a union
}[keyof Args & keyof Mapping]

El problema es que produce una unión { a: string } | { b : string }, pero lo que realmente queremos es una intersección { a: string } & { b: string }. Afortunadamente, esta respuesta tiene un tipo auxiliar para eso.

// Convert a union to an intersection 
intersection-type
type UnionToIntersection<U> = (U extends any
? (k: U) => void
: never) extends (k: infer I) => void
  ? I
  : never

Ahora hagamos un tipo para poner todo eso junto:

// Type of the return values, as an intersection.
type ReturnValueType<Args, Mapping> = 
  UnionToIntersection<ReturnValueTypeUnion<Args, Mapping>>

Y finalmente podemos escribir la función:

declare function main<T extends UsesArgs<MyMapping>>(
  opts: T,
): ReturnValueType<T, MyMapping>

// Examples
const a = main({ a: 'abc' }) // { aResult: string }
const b = main({ b: 'def' }) // { bResult: string }
const aPlusB = main({ a: 'abc', b: 'def' }) // { aResult: string } & { bResult: string }

Parque infantil