typescript - ¿Cómo puede uno especificar algunos argumentos de tipo genérico mientras que los otros se infieren?
Dado el ejemplo a continuación, ¿hay alguna manera de lograr un código DRY que sea similar a foobarA?
interface F<T, U extends string> { t: T, f: (u: U) => void }
declare const foo: <T, U extends string>(type: U) => F<T, U>;
// the following correctly types foobar, however 'bar' redundantly repeats.
const foobar = foo<string, 'bar'>('bar')
// ideally one would want the generic inference to be smarter and the
// following to work:
const foobarA = foo<string>('bar')
Respuesta #1
TypeScript no admite directamente la inferencia parcial de argumentos de tipo como se solicita en microsoft/TypeScript#26242. Por lo tanto, si tienes una función genérica de la forma:
declare const foo: <T, U extends string>(u: U) => F<T, U>;
no hay manera de llamarlo para que especifiques manualmente T pero dejes que el compilador infiera U:
// const foobarA: F<string, unknown> ☹
const foobar = foo<string, "bar">("bar");
O bien tienes que especificar manualmente ambos argumentos de tipo como en foo<string, "bar">("bar"); (lo cual, desafortunadamente, es redundante) o dejas que el compilador infiera ambos argumentos como en foo("bar") (lo cual no infiere el tipo correcto para T).
Hasta que esto cambie, necesitarás encontrar una manera de sortearlo.
Una solución alternativa es el currying, mediante el cual se divide la llamada a una función genérica en varias llamadas a funciones genéricas, de modo que se pueda especificar manualmente el argumento de tipo para la primera, y luego permitir que el compilador infiera el argumento de tipo para la segunda. Eso significa que necesitaríamos refactorizar el foo() anterior en
declare const foo: <T, >() => <U extends string>(u: U) => F<T, U>;
Y entonces podríamos llamar a la función así:
declare const foo: <T, >() => <U extends string>(u: U) => F<T, U>;
const foobarA = foo<string>()("bar");
// const foobarA: F<string, "bar">
Es un poco molesto tener que incluir esa llamada de función extra, pero sintácticamente al menos es solo otro par de paréntesis, y te da exactamente el comportamiento que estás buscando.
Otra solución alternativa es agregar un parámetro ficticio correspondiente al argumento de tipo que le gustaría especificar manualmente, y luego pasar un valor de ese tipo para que el compilador pueda inferirlo correctamente. Esto se ve así:
declare const foo: <T, U extends string>(dummyT: T, u: U) => F<T, U>
Y entonces podríamos llamar a la función así:
const foobarA = foo("someString", "bar");
// const foobarA: F<string, "bar">
El valor "someString" pasado para dummyT casi con certeza es ignorado por la implementación de la función, pero permite que el compilador infiera string para T. Si el llamador no tiene un valor del tipo a la mano, aún puede usar la versión ficticia afirmando que algo aleatorio es de ese tipo:
const foobarB = foo(null! as string, "bar");
// const foobarB: F<string, "bar">
Aquí he usado null!, el valor es null y al afirmar que no es null mediante el sufijo !, tenemos un imposible “null no nulo” del tipo never que puede ser asignado a cualquier otro tipo. El beneficio principal de una cosa tan desconcertante como null! es que solo toma cinco pulsaciones de tecla producir un valor del imposible tipo never, que puede ser afirmado as string sin que el compilador se queje.
Ambas versiones funcionan; tiendo a favorecer el currying a menos que el caso de uso haga que sea fácil para el llamador encontrar y pasar un argumento apropiado para el parámetro ficticio.
Enlace de Playground al código
Respuesta #2
Lo siguiente literalmente "funcionará": se compilará. No sé si realmente logrará lo que deseas.
const foo = <S,T extends string = string>(type:T)=>{
return {
[type]: (arg: S)=>console.log(arg)
}
}
