typescriptgenericstypescript-generics

Filtering tuples breaks recursion when used with generics


I am trying to get to the WANTED type from the definition of columns.

declare class ColumnDefinition<T, R extends boolean = false, N extends string = string> {
    typeValidator: (v: unknown) => v is T;
    required: R;
    name: N;
};

type columns = [
    ColumnDefinition<number, true, 'ID'>,
    ColumnDefinition<string, true, 'text'>,
    ColumnDefinition<boolean, false, 'bool'>,
    ColumnDefinition<bigint, false, 'bigint'>,
];

type WANTED = {
    ID: number;
    text: string;
    bool?: boolean | undefined;
    biging?: bigint | undefined;
};

I tried to look for TupleFilter methods, but they all seem to break when defining the recursive part.

type FilterColumns<
    Required extends boolean,
    COLUMNS extends readonly ColumnDefinition<any, boolean, any>[],
> = COLUMNS extends readonly [infer L, ...infer Rest]
    ? L extends ColumnDefinition<any, Required>
        ? [L, ...FilterColumns<Required, Rest>]
        : FilterColumns<Required, Rest>
    : [];

playground

In the code above specifically FilterColumns<Required, Rest> the Rest is highlighted and tells that unknown[] is not assignable to readonly ColumnDefinition<any, boolean, any>[].

Why is Rest defined as unknown[] instead of ColumnDefinition<number, boolean, any>[]? How to fix this?


Solution

  • You've run into the issue reported at microsoft/TypeScript#45281. When using conditional types to infer the rest element of a variadic tuple type, TypeScript doesn't re-constrain the resulting array type. It essentially falls back to unknown[], as you've seen.

    This was considered to be a bug when it was reported. TypeScript 4.7 introduced support for extends constraints on infer type variables, meaning that you can manually set the constraint yourself. Your code example therefore becomes

    type FilterColumns<
        Required extends boolean,
        COLUMNS extends readonly ColumnDefinition<any, boolean, any>[],
    > = COLUMNS extends readonly [infer L,
        ...infer Rest extends readonly ColumnDefinition<any, boolean, any>[]]
    //                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        ? L extends ColumnDefinition<any, Required>
        ? [L, ...FilterColumns<Required, Rest>]
        : FilterColumns<Required, Rest>
        : [];
    

    and it works as expected.

    The bug is on the Backlog, which generally means that pull requests from community members are welcomed. If you really want to see this fixed, you might want to fix it yourself and submit a pull request. Currently the issue only has a few upvotes and it can be worked around with an extends constraint, so it's not very likely that the TS team will decide to fix it directly.

    Playground link to code