typescript - Miért nem működik az alapértelmezett paraméterként megadott üres tömb generikus típusokkal?
Van egy függvényem, amely egy olyan argumentumot fogad el, amely alapértelmezettként egy üres tömb []:
function foo(arr: any[] = []) {
return arr;
}
Azonban ha generikumokat használok, típushiba lépne fel:
function foo<T extends any[]>(arr: T = []) { // ❌
return arr;
}
Type 'never[]' is not assignable to type 'T'.
'never[]' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'any[]'.
Miért okoz típushibát az általános típusokra váltás?
Válasz #1
Én is belefutottam ebbe a problémába, és a megoldás, amit kitaláltam, az volt, hogy overloadokat használjak ahelyett, hogy alapértelmezett argumentumértéket adnék. Így a TypeScript tudja, hogy az esetet, amikor az argumentum hiányzik, másképp kezelje, mint amikor az argumentum jelen van.
Az Ön esetében ez így nézhet ki:
function foo(): any[]
function foo<T extends any[]>(arr: T): T
function foo<T extends any[]>(arr?: T): T | any[] {
if (!arr) {
return [];
} else {
return arr;
}
}
foo(); // <- Returns any[]
foo<string[]>(); // <- Not allowed
foo(['test']); // <- return type string[]
TypeScript Játszótér
Ahogy @jcalz is elmagyarázta a kérdésedhez fűzött hozzászólásaiban, a probléma abból fakad, hogy lehetséges egy generikus típus egyértelmű megadása a generikus függvény hívásakor. Például:
function foo<T extends any[]>(arr: T = []) {
return arr;
}
foo<string[]>();
TypeScript Játszótér
A legnyilvánvalóbb esetekben, mint a fenti string[], ez rendben van, mert a [] lehet bármilyen típusú tömb. De vannak más típusok is, amelyek megfelelnek a extends any[] megszorításnak, de nem alkalmazhatók a []-re. Az a példa egy tuple-re, amit @jcalz használt a kommentjeiben, ennek egy példája:
function foo<T extends any[]>(arr: T = []) {
return arr;
}
foo<[string]>();
TypeScript Játszótér
Ebben az esetben az általános függvényének [string] típusú értéket kellene visszaadnia, de az általa visszaadott érték [], ami nem az a típus. Annak ellenére, hogy mind a [string], mind a [] megfelel a extends any[] feltételnek.
Ezért az általam bemutatott túlterheléses megoldás csak akkor teszi a függvényt generikussá, ha az argumentum jelen van, így a generikus típus mindig ebből az argumentumból következtethető. Az opcionális argumentum használata helyett az alapértelmezett érték a függvényen belül van beállítva.
A kérdésben használt konkrét példában az @jcalz kommentjében javasolt, korlátozások nélküli egyszerűsített generikus függvény jól fog működni:
function foo<T>(arr: T[] = []) { return arr; }
Azonban vannak bonyolultabb esetek is, ahol az általam javasolt túlterhelt metódus hasznosabb lehet. Itt van egy egyszerűsített példa egy olyan helyre, ahol használtam, ahol a túlterhelések alkalmazásának hasznossága abból adódott, hogy megőriztem egy feltételezett string unión típusát, és továbbadtam azt a visszatérési értékhez.
Sajnálom ennek a példának a bonyolultságát. Ez egy kis darab egy összetettebb könyvtárból, amit éppen részben újraépítek, és nehezen sikerült ennél sokkal jobban izolálni és egyszerűsíteni.
A fontos rész az, hogy a túlterhelt függvény egy generikus típust ad vissza, és azt a típust helyesen kell kikövetkeztetni, függetlenül attól, hogy átadtak-e argumentumot vagy sem. Ebben az esetben a kikövetkeztetett típus egy string unió, amely különösen hasznos lehet az automatikus kiegészítésnél, különösen akkor, ha sok stringet tartalmaz.
/**
* A function for summarising a set of numbers
*/
type Summariser<T = any, G = any> = (numbers: number[], groupName: G) => T;
/**
* A group of Summariser functions
*/
type Summarisers<SummaryName extends string> = Record<SummaryName, Summariser>;
type DefaultSummaryName = 'Count';
const defaultSummarisers: Summarisers<DefaultSummaryName> = {
Count: (numbers: number[]) => numbers.length
} as const;
/**
* A 2D array of the results of Summariser functions applied to an Group of numbers,
* able to be printed to the console using `console.table`.
*
* The header row starts with "Value", then lists the names of each summary.
* Subsequent rows list the name of the set of numbers, then list each of its summaries.
*/
type Summary<SummaryName extends string> = [['Value', ...SummaryName[]], ...[string, ...any[]][]];
/**
* Create a 2D summary array that can be printed using console.table.
*/
function summarise(numMap: Map<string, number[]>): Summary<DefaultSummaryName>
function summarise<SummaryName extends string>(numMap: Map<string, number[]>, summarisers: Summarisers<SummaryName>): Summary<SummaryName>
function summarise<SummaryName extends string>(numMap: Map<string, number[]>, summarisersArg?: Summarisers<SummaryName>): Summary<DefaultSummaryName> | Summary<SummaryName> {
// If there was no argument, use a default value instead. This will affect the return type, as per the overloads
const summarisers = summarisersArg ?? defaultSummarisers;
const summaryNames = Object.keys(summarisers) as DefaultSummaryName[] | SummaryName[];
const summaryHeaderRow = ['Value', ...summaryNames] as const;
let summaryValuenumbers: [string, ...any[]][] = [];
for (let [groupName, numbers] of numMap.entries()) {
const summaryRow: [string, ...any[]] = [groupName];
for (let [, summariser] of Object.entries<Summariser>(summarisers)) {
const numbersSummary = summariser(numbers, groupName);
summaryRow.push(numbersSummary);
}
summaryValuenumbers.push(summaryRow);
}
const summary = [summaryHeaderRow, ...summaryValuenumbers];
// Let the overloads tell TypeScript which type the summary actually is.
// If there was no `summarisersArg` argument, it will be Summary<DefaultSummaryNames>,
// otherwise the type T could be inferred so no default was necessary and it will be Summary<SummaryName>
return summary as Summary<DefaultSummaryName> | Summary<SummaryName>;
}
//////////
const exampleGroup = new Map([
['Row 1', [1, 2, 3, 4, 5]],
['Row 2', [6, 7, 8, 9, 0]],
['Row 3', [0, 2, 4, 8]],
]);
console.log(exampleGroup);
const exampleSummary1 = summarise(exampleGroup); // <- Correctly inferred type as Summary<"Count">
// console.table doesn't work in the TypeScript Playground, so use console.log instead
console.log(exampleSummary1);
const summarisers = {
sum: (numbers: number[]) => {
if (numbers.every((el): el is number => typeof el === 'number')) {
return numbers.reduce((sum, el) => sum + el, 0);
} else {
return null;
}
}
};
const exampleSummary2 = summarise(exampleGroup, summarisers); // <- Correctly inferred type as Summary<"sum">
console.log(exampleSummary2);
TypeScript Játszótér
