typescript - ทำไมค่าพารามิเตอร์เริ่มต้นที่เป็นอาร์เรย์ว่างถึงไม่ทำงานกับชนิดข้อมูลเจเนอริก?
ฉันมีฟังก์ชันที่รับอาร์กิวเมนต์ซึ่งมีค่าเริ่มต้นเป็นอาร์เรย์ว่าง []:
function foo(arr: any[] = []) {
return arr;
}
อย่างไรก็ตามถ้าฉันใช้เจนเนอริก จะเกิดข้อผิดพลาดเกี่ยวกับประเภท:
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[]'.
ทำไมการเปลี่ยนไปใช้เจนเนอริกส์ถึงทำให้เกิดข้อผิดพลาดเกี่ยวกับประเภทข้อมูล?
คำตอบ #1
ฉันก็เจอปัญหานี้เช่นกัน และวิธีแก้ปัญหาที่ฉันคิดได้คือการใช้การโอเวอร์โหลดแทนการใช้ค่าตัวแปรเริ่มต้น ด้วยวิธีนี้ TypeScript จะรู้ว่าต้องจัดการกรณีที่ไม่มีอาร์กิวเมนต์แตกต่างจากเมื่อมีอาร์กิวเมนต์
ในกรณีของคุณ มันอาจจะดูเหมือนแบบนี้:
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
ดังที่ @jcalz ได้อธิบายในความคิดเห็นของเขาในคำถามของคุณ ปัญหานี้เกิดจากความเป็นไปได้ที่จะระบุชนิดทั่วไปโดยตรงเมื่อเรียกใช้ฟังก์ชันทั่วไป ตัวอย่างเช่น:
function foo<T extends any[]>(arr: T = []) {
return arr;
}
foo<string[]>();
สนามเด็กเล่น TypeScript
ในกรณีที่ชัดเจนที่สุด เช่น string[] ข้างต้น นี่เป็นเรื่องปกติ เพราะ [] สามารถเป็นอาร์เรย์ของชนิดใดก็ได้ แต่ยังมีชนิดอื่น ๆ ที่ตรงตามข้อจำกัด extends any[] ซึ่งไม่สามารถนำไปใช้กับ [] ได้ ตัวอย่างของทูเพิลที่ @jcalz ใช้ในความคิดเห็นของพวกเขาคือตัวอย่างหนึ่งของเรื่องนี้:
function foo<T extends any[]>(arr: T = []) {
return arr;
}
foo<[string]>();
สนามเด็กเล่น TypeScript
ในกรณีนี้ ฟังก์ชันทั่วไปของคุณควรคืนค่าประเภท [string] แต่ค่าที่มันคืนมาคือ [] ซึ่งไม่ใช่ประเภทนั้น แม้ว่าทั้ง [string] และ [] จะตรงกับข้อจำกัด extends any[] ก็ตาม
นั่นคือเหตุผลที่วิธีแก้ปัญหาการโอเวอร์โหลดที่ฉันนำเสนอนั้นทำให้ฟังก์ชันเป็นประเภททั่วไปเฉพาะเมื่อมีอาร์กิวเมนต์ ดังนั้นประเภททั่วไปจึงสามารถอนุมานได้เสมอจากอาร์กิวเมนต์นั้น แทนที่จะใช้อาร์กิวเมนต์ตัวเลือก ค่าดีฟอลต์จะถูกตั้งภายในฟังก์ชัน
ในตัวอย่างเฉพาะที่ใช้ในคำถามนี้ ฟังก์ชันทั่วไปแบบง่ายที่ไม่มีข้อจำกัดซึ่งถูกเสนอโดย @jcalz ในความคิดเห็นจะทำงานได้ดี:
function foo<T>(arr: T[] = []) { return arr; }
อย่างไรก็ตาม มีกรณีที่ซับซ้อนมากกว่านี้ซึ่งวิธีการโอเวอร์โหลดที่ฉันแนะนำอาจมีประโยชน์มากกว่า นี่คือตัวอย่างที่ง่ายขึ้นของหนึ่งกรณีที่ฉันใช้มัน ซึ่งประโยชน์ของการใช้โอเวอร์โหลดมาพร้อมกับการรักษาประเภทสตริงยูเนียนที่สันนิษฐานไว้และส่งต่อไปยังค่าที่ส่งกลับ
ขอโทษสำหรับความซับซ้อนของตัวอย่างนี้ มันเป็นส่วนเล็ก ๆ จากไลบรารีที่ซับซ้อนมากขึ้นซึ่งฉันกำลังสร้างใหม่ และฉันพยายามแยกและทำให้มันง่ายขึ้นมากกว่านี้แต่ก็ยังลำบากอยู่
ส่วนสำคัญคือฟังก์ชันที่มีการโอเวอร์โหลดต้องส่งค่ากลับเป็นชนิดทั่วไป และชนิดนั้นจำเป็นต้องถูกอนุมานได้อย่างถูกต้องไม่ว่ามีการส่งอาร์กิวเมนต์เข้ามาหรือไม่ ในกรณีนี้ ชนิดที่ถูกอนุมานคือการรวมกันของสตริง ซึ่งสามารถมีประโยชน์มากสำหรับการเติมข้อความอัตโนมัติ โดยเฉพาะเมื่อมันรวมสตริงจำนวนมาก
/**
* 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
