ค้นหาเว็บไซต์

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

บทความที่เกี่ยวข้อง