sobrecarga - ¿Cómo decirle a TypeScript que dos tipos genéricos son iguales?
Considera la siguiente función sobrecargada.
function scan<A>(this: A[], f: (a: A, x: A) => A): A[];
function scan<A, B>(this: A[], f: (a: B, x: A) => B, init?: B): B[] {
if (init === undefined) {
const result = [this[0]];
for (let i = 1; i < this.length; i++) {
result.push(f(result[i - 1], this[i]));
}
return result;
}
const result = [init];
for (let i = 0; i < this.length; i++) {
result.push(f(result[i], this[i]));
}
return result;
}
Tenga en cuenta que cuando no se proporciona init, el tipo genérico B debería ser el mismo que A. ¿Cómo le digo esto a TypeScript? Actualmente, TypeScript se queja de que A no se puede asignar a B y viceversa.
Respuesta #1
Una función sobrecargada tiene un conjunto de declaraciones de firma de llamada que determinan cómo se puede llamar a la función y (suponiendo que la función esté implementada y no solo declarada) una sola implementación. La firma de implementación no es llamable.
En tu código de ejemplo, tienes una sola firma de llamada
// call signature
function scan<A>(this: A[], f: (a: A, x: A) => A): A[];
y una implementación
// implementation
function scan<A, B>(this: A[], f: (a: B, x: A) => B, init?: B): B[] {
/* snip */
}
Pero esto no parece ser lo que pretendes. Realmente quieres que esas firmas sean ambas firmas de llamada, así:
// call signatutes
function scan<A>(this: A[], f: (a: A, x: A) => A): A[];
function scan<A, B>(this: A[], f: (a: B, x: A) => B, init?: B): B[];
// implementation
function scan(...) {
Entonces la pregunta es: ¿cuál debería ser la firma de la implementación?
El compilador de TypeScript no puede verificar la implementación revisándola contra cada firma de llamada por separado. Hubo una sugerencia de hacer esto en microsoft/TypeScript#13235, pero se cerró por ser demasiado complejo de implementar. En cambio, lo que el compilador hace es asegurarse de que los parámetros de la firma de implementación puedan manejar los parámetros de cada una de las firmas de llamada, y asegurarse de que el tipo de retorno de la firma de implementación pueda manejar los tipos de retorno de cada una de las firmas de llamada. Es decir, el tipo de retorno puede ser la unión de todos los tipos de retorno de las firmas de llamada. Esto no es seguro con respecto al tipo (porque podrías estar devolviendo el tipo incorrecto para una firma de llamada en particular), pero es conveniente.
Para bien o para mal, esa verificación laxa es la forma en que funciona la implementación de sobrecarga de TypeScript. Así que necesitas tener cuidado cuando escribas funciones sobrecargadas.
De todos modos, eso significa que la implementación necesita ser algo así:
// implementation signature
function scan<A, B>(this: A[], f: (a: B | A, x: A) => A, init?: B | A) {
if (init === undefined) {
const result = [this[0]];
for (let i = 1; i < this.length; i++) {
result.push(f(result[i - 1], this[i]));
}
return result;
}
const result = [init];
for (let i = 0; i < this.length; i++) {
result.push(f(result[i], this[i]));
}
return result;
}
No es perfecto de ninguna manera, pero probablemente sea lo mejor que podamos conseguir si queremos poner estos dos comportamientos separados en una sola función sobrecargada.
Enlace de Playground al código
Respuesta #2
Pude hacer que esto pasara la verificación de tipos debilitando la firma de implementación. Lección aprendida, la firma de implementación siempre debe ser una combinación de las firmas sobrecargadas.
const SCAN_EMPTY_NO_INIT = 'Scan of empty array with no initial value';
type Reducer<A, B> = (acc: B, a: A) => B;
function scan<A>(this: A[], f: Reducer<A, A>): A[];
function scan<A, B>(this: A[], f: Reducer<A, B>, init: B): B[];
function scan<A, B>(this: A[], f: Reducer<A, A | B>, init?: B): (A | B)[] {
const { length } = this;
let i = 0, result = [typeof init === 'undefined' ? this[i++] : init];
if (length < i) throw new TypeError(SCAN_EMPTY_NO_INIT);
const j = i;
while (i < length) {
result.push(f(result[i - j], this[i]));
i++;
}
return result;
}
Tenga en cuenta que la función no puede ser llamada con la firma de implementación. Por lo tanto, no puede usar esta función de maneras no intencionadas, para crear un arreglo de As o Bs.
