I have a function that accepts an argument that defaults to an empty array []
:
function foo(arr: any[] = []) {
return arr;
}
However if I use generics, there would be a type error:
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[]'.
Why does changing to generics cause a type error?
I've run into this problem as well, and the solution I came up with was to use overloads instead of a default argument value. That way, TypeScript knows to handle the case where the argument is absent differently from when the argument is present.
In your case, that might look like this:
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 Playground
As @jcalz has explained in comments on your question, the issue stems from the fact that it's possible to explicitly specify a generic type when calling a generic function. For example:
function foo<T extends any[]>(arr: T = []) {
return arr;
}
foo<string[]>();
TypeScript Playground
In the most obvious cases, like string[]
above, this is fine because []
can be an array of any type. But there are other types that fit the extends any[]
constraint which cannot be applied to []
. The example of a tuple that @jcalz used in their comments is an example of this:
function foo<T extends any[]>(arr: T = []) {
return arr;
}
foo<[string]>();
TypeScript Playground
In this case, your generic function should return a value of type [string]
, but the value it returns is []
which is not of that type. Even though both [string]
and []
both match the extends any[]
constraint.
That's why the overload solution I've presented makes the function only generic if the argument is present, so the generic type can always be inferred from that argument. Instead of using an optional argument, the default value is set within the function.
In the specific example used in this question, the simplified generic function with no constraints suggested by @jcalz in a comment will work fine:
function foo<T>(arr: T[] = []) { return arr; }
However, there are more complex cases where the overload method I've suggested can be more useful. Here is a simplified example of one place where I've used it, where the utility of using overloads came with maintaining an inferred string union type and passing it through to the return value.
Sorry for the complexity of this example. It's a small piece from a more complex library that I'm part way through rebuilding, and I've struggled to isolate and simplify it much further than this.
The important part is that the overloaded function returns a generic type, and that type needs to be inferred correctly whether an argument was passed or not. In this case, the inferred type is a string union, which can be very useful for autocomplete particularly when it includes many strings.
/**
* 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 Playground
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With