överladdning - Hur talar man om för TypeScript att två generiska typer är samma?
Överväg följande överlagrade funktion.
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;
}
Observera att när init inte anges, bör den generiska typen B vara samma som A. Hur berättar jag detta för TypeScript? För närvarande klagar TypeScript att A inte kan tilldelas B och vice versa.
Svar #1
En överlagrad funktion har en uppsättning anropssignaturdeklarationer som bestämmer hur funktionen kan anropas, och (förutsatt att funktionen är implementerad och inte bara deklarerad) en enda implementation. Implementationssignaturen kan inte anropas.
I ditt exempel på kod har du en enda anropssignatur
// call signature
function scan<A>(this: A[], f: (a: A, x: A) => A): A[];
och en implementering
// implementation
function scan<A, B>(this: A[], f: (a: B, x: A) => B, init?: B): B[] {
/* snip */
}
Men detta verkar inte vara vad du avser. Du vill verkligen att de signaturerna båda ska vara anropningssignaturer, som detta:
// 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(...) {
Så frågan är: vad ska implementeringssignaturen vara?
TypeScripts kompilator kan inte kontrollera implementationen genom att kontrollera den mot varje anropssignatur separat. Det fanns ett förslag att göra detta på microsoft/TypeScript#13235, men det stängdes eftersom det var för komplext att implementera. Istället säkerställer kompilatorn att implementationssignaturens parametrar kan hantera parametrarna från varje av anropssignaturerna, och säkerställer att implementationssignaturens returtyp kan hantera returtyperna från varje av anropssignaturerna. Det vill säga, returtypen kan vara unionen av alla anropssignaturers returtyper. Detta är inte typesäkert (eftersom du kan returnera fel typ för en viss anropssignatur), men det är praktiskt.
För bättre eller sämre är en sådan lös kontroll det sätt på vilket TypeScripts överlagringsimplementering fungerar. Så du behöver vara försiktig när du skriver överlagrade funktioner.
Hur som helst betyder det att implementeringen behöver vara något i den här stilen:
// 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;
}
Det är på inga sätt perfekt, men det är förmodligen det bästa vi kan få om vi vill lägga dessa två separata beteenden i en enda överbelastad funktion.
Playground-länk till kod
Svar #2
Jag kunde få detta att typkontrollera genom att försvaga implementeringssignaturen. Lärdom lärd, implementeringssignaturen måste alltid vara en kombination av de överlagrade signaturerna.
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;
}
Observera att funktionen inte kan anropas med implementationssignaturen. Därför kan du inte använda denna funktion på oavsiktliga sätt, för att skapa en array av A eller B.
