typescripttypescript-generics

Generic function that accepts and returns array preserving mutablity


Original code that does not accept readonly array:

public transform<T>(value: T[], options?: (item: T) => boolean): T[] {
    if (!Array.isArray(value) || value.length <= 1 || options == null) {
        return value;
    }
    return value.filter(options);
}

First iteration:

public transform<
    T extends unknown[] | readonly unknown[],
    E = T extends Array<infer U> ? U : T extends ReadonlyArray<infer U> ? U : never
>(value: T, options?: (item: E) => boolean): T {
    if (!Array.isArray(value) || value.length <= 1 || options == null) {
        return value;
    }
    return value.filter(options) as T;
}

Has cast to T, incorrect return type.

Second iteration:

public transform<T extends readonly unknown[]>(
    value: T,
    options?: (item: T[number]) => boolean
): T extends unknown[] ? T[number][] : readonly T[number][] {
    if (!Array.isArray(value) || value.length <= 1 || options == null) {
        return value as any;
    }
    return value.filter(options) as any;
}

Has cast to any.


Solution

  • Without using a cast, I believe the simplest solution would be to use an overload.

    // mutable interface where a mutable array stays mutable
    function transform<T>(value: T[], options?: (item: T) => boolean): T[];
    // specific readonly interface where a readonly array stays readonly
    function transform<T>(
            value: readonly T[], options?: (item: T) => boolean
    ): readonly T[];
    // implementation has to explicitly accept a readonly array or you'll get errors
    function transform<T>(
            value: T[] | readonly T[],
            options?: (item: T) => boolean
    ): T[] | readonly T[] {
        if (!Array.isArray(value) || value.length <= 1 || !options) {
            return value;
        }
        return value.filter(options);
    }
    

    NB. You MUST provide the mutable overload, and it MUST come before the readonly overload. Otherwise, the compiler will happily choose the readonly overload for mutable arrays. That is, mutable arrays can do everything a readonly array can do. For instance, this is what would happen if the readonly implementation came first:

    let items: number[] = [1, 2, 3];
    items[0] = 0; // works
    
    let transformed = transform(items);
    transformed[0] = 0;
    // ^-- Index signature in type 'readonly number[]' only permits reading.(2542)