Search code examples
typescriptstring-literals

TS type that requires string literal in the array/tuple


I am trying to build a generic type, that will guarantee presence of the string literal (no matter at what position)

What I desire for:

// 'a' is in the first position
const x1: ArrayContainingLiteral<'a'> = ['a', 'b', 'c']; // ✅
// 'a' is in the last position
const x2: ArrayContainingLiteral<'a'> = ['b', 'a']; // ✅
// 'a' is in between of other elements
const x3: ArrayContainingLiteral<'a'> = ['b', 'a', 'c', 'd']; // ✅

// 'a' is not in the array
const x4: ArrayContainingLiteral<'a'> = ['b', 'c']; // ❌
// 'a' is not in the array (array is empty)
const x5: ArrayContainingLiteral<'a'> = []; // ❌

My current progress is:

export type DoesArrayIncludeLiteral<StringLiteral extends string, StringArray extends Array<string>> = {
  [ArrayIndex in keyof StringArray]: StringArray[ArrayIndex] extends StringLiteral ? unknown : never;
}[number] extends never
  ? false
  : true;

export type ArrayContainingLiteral<Literal extends string, StringArray extends Array<string> = Array<string>> =
  DoesArrayIncludeLiteral<Literal, StringArray> extends true ? StringArray : never;

Which works somewhat fine but requires the whole array to be passed as an type argument

const x1: ArrayContainingLiteral<'a', ['a', 'b', 'c']> = ['a', 'b', 'c']; // ✅

Solution

  • This can be done for arrays under a certain size, for example:

    type Indices = 0 | 1 | 2 | 3 | 4 | 5;
    
    type ArrayContainingLiteral<T> =
      Array<unknown> &
      (Indices extends infer U extends number ?
         U extends unknown ?
           { [I in U]: T }
         : never
      : never)
    

    For larger sizes, see Is it possible to restrict number to a certain range

    import { IntRange } from 'type-fest'
    
    type Indices = IntRange<0, 32>
    

    A naive solution like:

    type ArrayContainingLiteral<T> =
      | [T, ...unknown[]]
      | [unknown, ...ArrayContainingLiteral<T>]
    

    Does not work because of infinite recursion.