typescripttuplestype-inferencevariadic-tuple-types

typescript defining type of element in array based of the types of the previous elements


an example for such usecase would be:

type OnlyExtendArr = ???;
const arr1: OnlyExtendArr =
[
    {},  //ok
    {name: 'name'},  //ok
    {name:"name",age: 'age'},  //ok
    {age: 'age'} // should error - 'name' is required so this item's type would extend previous type  
];

in this example, I want that the type of each element would extend the previous type.

more practically I want to define a type where's the item in the index n should extend the item in the index n-1 but I don't know how to write it.

is it possible using typescript?

a good start would be extracting the type of the last element in the array, which seems to be not so simple (because you can't access the -1 element in typescript) (solution for this here)

Last element in the array:

type LengthOfTuple<T extends any[]> = T extends { length: infer L } ? L : never;
type DropFirstInTuple<T extends any[]> = ((...args: T) => any) extends (arg: any, ...rest: infer U) => any ? U : T;
type LastInTuple<T extends any[]> = T[LengthOfTuple<DropFirstInTuple<T>>];
type Stuff = [number, boolean, '10'];
type last  = LastInTuple<Stuff> //'10'

Solution

  • Yes, it is possible in typescript.

    type Last<T> = T extends [...infer _, infer L] ? L : never
    type First<T> = T extends [infer Head, ...infer _] ? Head : never
    type Tail<T> = T extends [infer _, ...infer Tail] ? Tail : never
    type ReplaceFirst<T> = [[never], ...Tail<T>]
    
    type Validator<T extends Array<any>, Result extends Array<any> = []> =
      (T extends []
        ? Result
        : (T extends [infer Head]
          ? (Head extends Last<Result> ? [...Result, Head]
            : [...Result, never]
          )
          : (T extends [infer Head, ...infer Rest]
            ? (First<Rest> extends Head
              ? Validator<Rest, [...Result, Head]>
              : Validator<ReplaceFirst<Rest>, [...Result, Head]>)
            : never)
        )
    
      )
    
    
    const builder = <
      Prop extends PropertyKey,
      Value extends string,
      Elem extends Record<Prop, Value>,
      Tuple extends Elem[]
    >(tuple: [...Tuple] & Validator<[...Tuple]>) => tuple
    
    const result = builder([
      {},  //ok
      { name: 'name' },  //ok
      { name: "name", age: 'age' },  //ok
      { age: 'age' } // error
    ])
    
    

    Playground

    Elem - infered element from the array/tuple

    Tuple - infered array/tuple

    Validator - iterates throught the infered tuple/array and checks whether next element (Rest[0] extends Head) extends previous one. If yes, call recursive Validator with this element, otherwise call Validator with never instead of invalid element. 'T extends [infer Head]' - before the last call, checks whether element extends last element from Result. If yes - push Element to Result and return Result, otherwise push never to Result.

    I have used [...Tuple] & Validator<[...Tuple]> to merge validated tuple with provided as an argument. In this way TS is able to highlight only invelid argument instead of whole argument.

    AFAIK, it it impossible to do without extra function, because you need to infer each element.

    If you want to extract last element from the array, you can do this:

    type Last<T> = T extends [...infer _, infer L] ? L : never
    
    type First<T> = T extends [infer Head, ...infer Tail] ? Head : never
    
    

    Please see documentation for variadic tuple types.

    If you are interested in tuple manipulation, you can check my article

    P.S. This solution was ok before variadic tuple types.