Weboldal keresés

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

Kapcsolódó cikkek