Home > Back-end >  How can you ensure that tuple element labels are preserved?
How can you ensure that tuple element labels are preserved?

Time:11-16

Background

I'm trying to replace an existing overloaded function with rest parameters using labeled tuple elements.

Original code

This is a simplified version of the original overloaded function:

function doSomething(arg1: string, arg2: number):void;
function doSomething(arg1: string, arg2: number, arg3: unknown[]):void;
function doSomething(arg1: string, arg2: number, arg3: boolean):void;
function doSomething(arg1: string, arg2: number, arg3: unknown[], arg4: boolean):void {
    // ...implementation
}

So the third and fourth arguments are optional but when supplied there order can be:

arg3: unknown[]
arg3: unknown[], arg4: boolean
arg3: boolean

Attempted solution

I decided to create the labeled tuple elements first and then depending on the supplied type variable types either set their type to the supplied type or to never. Then filter that tuple removing any element that's never and return the result.

type CalcAdditionArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> = [
    myArg3: ThirdArgType extends [] ? never : ThirdArgType,
    myArg4 : [FourthArgType] extends [boolean] ? FourthArgType extends true ? true : never : never
]

type GetAdditionalArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> = 
    FilterType<CalcAdditionArgs<ThirdArgType, FourthArgType>,  never>

FilterType is a modified version of the tuple filtering utility found here https://stackoverflow.com/a/64034671/1798234

type FilterType<T extends unknown[], U = undefined> = 
    (T extends [] ? 
        [] :
        (T extends [infer H, ...infer R] ?
            ([H] extends [U] ?
                FilterType<R, U> :
                [H, ...FilterType<R, U>]) : 
            T
        )
    );

Just for clarity, this is the function that uses them

function execute<
    ThirdArgType extends unknown[] = [],
    FourthArgType extends boolean = false
>(
    arg1: string,
    arg2: number,
    ...args:GetAdditionalArgs<ThirdArgType, FourthArgType>
    
): void {
    // do something here
}

Problem

Playground

This is the output of the 2 utility types:

type a = CalcAdditionArgs<[], false>; //  [myArg3: never, myArg4: never]
type b = CalcAdditionArgs<[], true>; // [myArg3: [string, number], myArg4: never]
type c = CalcAdditionArgs<[string, number], false>; // [myArg3: [string, number], myArg4: never]
type d = CalcAdditionArgs<[string, number, Function], true>; // [myArg3: [string, number, Function], myArg4: true]

type e = GetAdditionalArgs<[], false>; // []
type f = GetAdditionalArgs<[], true>; // [true]
type g = GetAdditionalArgs<[string, number], false>; // [string: number]
type h = GetAdditionalArgs<[string, number, Function], true>; // [[string, number, Function], true]

As you can see, GetAdditionalArgs (or rather FilterType) is stripping the tuple element labels.

Question

I'm unable to understand how - if it's actually possible - to go about creating and then manipulating a tuple type within a utility type. i.e create an empty tuple and then add the required types to it. Hence my approach to the solution to create the populated tuple up front and then remove elements instead.

  1. Does anyone know why the tuple element labels are being stripped by FilterType and is there a way to fix this using the existing solution?

Or

  1. Is there a better/simpler solution to achieve the result i'm looking for?

Solution

Playground

Thanks to the response from @captain-yossarian and this SO answer from @ford04 (https://stackoverflow.com/a/64194372/1798234) i was able to rethink my approach to the solution using rest parameters:

type Append<E extends [unknown], A extends unknown[]> = [...A, ...E]

type GetMyArrayArg<T extends unknown[]> = [myArrayArg: T extends [] ? never : T]
type GetMyBooleanArg<T extends boolean> = [myBooleanArg : [T] extends [boolean] ? T extends true ? true : never : never]


type AddParameter<T extends [unknown], U extends unknown[] = []> =
    T extends [] ? 
        U :
        T extends [infer H] ?
            [H] extends [never] ? 
                U : 
                Append<T, U> :
            U

type GetAdditionalArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> = 
    AddParameter<
        GetMyBooleanArg<FourthArgType>, 
        AddParameter<
            GetMyArrayArg<ThirdArgType>>
        >


type a = GetAdditionalArgs<[], false>; // []
type b = GetAdditionalArgs<[], true>; // [myBooleanArg: true]
type c = GetAdditionalArgs<[string, number], false>; // [myArrayArg: [string, number]]
type d = GetAdditionalArgs<[string, number, Function], true>; // [myArrayArg: [string, number, Function], myBooleanArg: true]

CodePudding user response:

If you are curious why tuple labels are removed from output it might be hard to answer.

First of all, here you can find docs .

According to docs:

They’re purely there for documentation and tooling

This is only my guess.

I think labels are removed because of FilterType utility type. FilterType creates completely new tuple and seems to be that TS does not preserve tuple labels during iteration.

Consider this example:

type Labeled = [name: string];

type Infer<L extends Labeled> = L extends [infer First] ? [First] : never

type Result = Infer<Labeled> // [string], no label

Number indexing does not preserve labels either. [L[0]] - returns [string].

Seems to be that it works with rest parameters:

type Labeled = [name: string];

type Infer<L extends Labeled> = L extends [infer _] ? [...L] : never

type Result = Infer<Labeled> // [name: string], with label

CalcAdditionArgs does not create new tuple.

I did not find in docs how to preserve the labels.

I assume, that since you create new tuple in FilterType, the order of elements might not be preserved. Hence it might be unsafe to keep labels. But it is only my guess.

Here you can find PR


However, you can add labels into FilterType:

type FilterType<T extends unknown[], U = undefined> =
    (T extends [] ?
        [] :
        (T extends [infer H, ...infer R] ?
            ([H] extends [U] ?
                FilterType<R, U> :
                [Batman: H, ...Superman: FilterType<R, U>]) : // <----- LABELS
            T
        )
    );

As you might have noticed, I have added Batman label and Superman labels and they are exists in the output:

type FilterType<T extends unknown[], U = undefined> =
    (T extends [] ?
        [] :
        (T extends [infer H, ...infer R] ?
            ([H] extends [U] ?
                FilterType<R, U> :
                [Batman: H, ...Superman: FilterType<R, U>]) :
            T
        )
    );


type CalcAdditionArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> = [
    myArg3: ThirdArgType extends [] ? never : ThirdArgType,
    myArg4: [FourthArgType] extends [boolean] ? FourthArgType extends true ? true : never : never
]

type GetAdditionalArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> =
    FilterType<CalcAdditionArgs<ThirdArgType, FourthArgType>, never>


type e = GetAdditionalArgs<[], false>; // []
type f = GetAdditionalArgs<[], true>; // [Batman: true]
type g = GetAdditionalArgs<[string, number], false>; // [Batman: [string, number]]
type h = GetAdditionalArgs<[string, number, Function], true>; // [Batman: [string, number, Function], Batman: true]

If this does not meet your expectations, you can just create manually a union of allowed tuples. It will be safest way

  • Related