Arguments of same length Typescript

I'd like to write a function in Typescript as follows

x, y => void

Where x is an array of type string[] and y is an array of type string[][], but I'd like to constrain y so that the inner arrays must have the same length as array x.

Is this possible?


UPDATE 2022 It might be done in a much easier and less verbose way:



function array<X extends unknown[], Y extends X[]>(x: [...X], y: [...Y]) {
    return [1, 2, 3] as any
}

const result = array([1, 2, 3], [[1, 1, 1]]) // ok

const result2 = array([1, 2, 3], [[1, 1, 1], [1, 1]]) // expected error

const result3 = array([1, 2, 3], [[1, 1, 1], [1]]) // expected error

const result4 = array([1, 2], [[1, 1], [1, 1]]) // ok

const result5 = array([1, 2], [[1, 1], [1]]) // expected error

I just was not aware about variadic tuple types 1 year ago :D

Yes, this is possible.

CONSIDER THIS SOLUTION AS A DEPRECATED ONE

Here is my solution:

/**
 * Second approach, is more advanced and it is exactly what you are looking for
 */
type ArrayElement = number
type Array1D = ReadonlyArray<ArrayElement>
type Array2D = ReadonlyArray<Array1D>

type MutableLength = unknown[]['length'] // number

/**
 * Get length of the array
 * Allow only immutable arrays
 */
export type Length<T extends ReadonlyArray<any>> = T extends { length: infer L } ? MutableLength extends L ? MutableLength : L : MutableLength;

/**
 * Compare length of the arrays
 */
type CompareLength<X extends ReadonlyArray<any>, Y extends ReadonlyArray<any>> =
    MutableLength extends Length<X> ? false : MutableLength extends Length<Y> ? false : Length<X> extends Length<Y> ? true : false;

/**
 * Check if all arrays (union type) have same length as X
 */
type Filter<X extends ReadonlyArray<any>, Y extends ReadonlyArray<any>> =
    X['length'] extends Y['length']
    ? Y['length'] extends X['length'] ? Y : never : never
{

    /**
     * CompareLength, compares length of X and filtered Y, 
     * if true - return zero index element - ReadonlyArray<ArrayElement>
     * if false - return never
     * 
     * So, if it will return never, then you can't pass second argument,
     * but if you did not provide second argument, you will receive another error - 
     * function expects two arguments
     */
    function array<X extends Array1D, Y extends {
        0: readonly ArrayElement[]
    }[CompareLength<X, Filter<X, Y>> extends true ? 0 : never]>(x: X, y: readonly Y[]): 'put here your returned type'

    function array<X extends Array1D, Y extends readonly ArrayElement[], Z extends CompareLength<X, Y>>(x: X, y: readonly Y[]) {
        return [1, 2, 3] as any
    }
    const result = array([1, 2, 3] as const, [[1, 1, 1], [1, 2, 3]] as const) // ok
    const result0 = array([1, 2, 3] as const, [[1, 1, 1]] as const) // ok

    const arr = [1, 2, 3] as const

    const result1 = array([1, 2, 3], [[1, 1, 1], [1]]) // error
    const result2 = array([1, 2, 3] as const, [[1, 1, 1], [1, 2]] as const) // error
    const result3 = array([1, 2, 3] as const, [[1, 2]] as const) // error
    const result4 = array([1, 2, 3] as const, [[1], [1, 2], [1, 2, 3]] as const) // error

    const result5 = array([1, 2, 3] as const, [1] as const) // error
    const result6 = array([1, 2, 3] as const, [[1, 2, 3], []] as const) // error
    const result7 = array(arr, [[1, 1, 1]]) // error, because TS is unable to fidure out length of mutable array. 
}

I used number instead of string, because it is easier to type numbers instead of strings.

Feel free to change it ArrayElement to string

Please keep in mind, that you should use every time as const to infer array length

Playground