Typescript: Add a generic way to specify length of a tuple type

Created on 6 Aug 2018  ยท  29Comments  ยท  Source: microsoft/TypeScript

Search Terms

tuple, type, length

Suggestion

I'd like to see a way to specify the length of a tuple or array type when declaring it. For short tuples, it may be viable to write them out by hand like [number, number], but whenever the length is longer (say 10+), it becomes a chore.

Using the following approach to define a fixed-length tuple

// @ts-ignore interface can only extend interface or class
interface FixedLengthArray<T extends any[], L extends number> extends T {
    length: L;
}

has a few drawbacks:
1.: we cannot extend T because its not an interface or class.
2.: when ignoring that error, tuple literals are not inferred as tuples but as arrays when assigning them:

type Foo = FixedLengthArray<number[], 5>;
const foo1: Foo = [1, 2, 3, 4, 5]; // error: length is not compatible, number not assignable to 5
const foo2: Foo = [1, 2, 3, 4, 5] as [1, 2, 3, 4, 5]; // works, but defeats the purpose of that fixed-length

On the other hand, when using manually-written tuples, this works:

type Bar = [number, number, number, number, number];
const bar1: Bar = [1, 2, 3, 4, 5]; // no error

Use Cases

  • Declaring fixed-length tuples or arrays with a large number of items
  • Creating a Range union (e.g. 0 | 1 | 2 | 3 | 4 from the keys of such a type) without having to resort to recursive types (currently broken, see #26155)

Examples

I'll leave the syntax up for discussion, and provide the length in the square brackets for now:

type T1 = number[3]; // equals [number, number, number]
type K1 = keyof T1; // 0 | 1 | 2
type T2 = number[100]; // equals [number, number, ..., number] (100 items total)
type K2 = keyof T2; // 0 | 1 | ... | 99
type T3 = any[0]; // equals [ ], type annotation is somewhat obsolete here
type K3 = keyof T3; // never

The basic rules for tuple assignability should apply, as if the types were written out by hand.

Related:

https://github.com/Microsoft/TypeScript/issues/24350
https://github.com/Microsoft/TypeScript/issues/18471

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. new expression-level syntax)
Awaiting More Feedback Fix Available Suggestion

Most helpful comment

Those of us who want full-blown dependent types for tuples (for things like type-safe immutable tuple push/pop/concat/split/etc) should give this a big ๐Ÿ‘ .

Is there a canonical issue for dependent types for tuples? Probably needs something like one or more of:

  • compile-time number arithmetic (e.g., Add<2,2> ~ 4)
  • compile-time number comparison (e.g., LessThan<0,3> ~ true)
  • compile-time conversion between numeric string literals and number literals, (e.g., StrToNum<"0"> ~ 0)
  • compile-time specification of strict tuples of a given length (e.g., TupleLen<3, string> ~ [string, string, string]) โ† we are here
  • compile-time specification of open-ended tuples of a given prefix length (e.g., OpenTupleLen<1, string> ~ [string, ...string[]])
  • compile-time specification of (strict or open) tuples with optional elements starting at some index (e.g., TupleLenOptional<3, string, 1> ~ [string, string?, string?])

Most of the above can be faked up, at least for nonnegative integers less than some bound, via a bunch of hardcoded lookup types (or via the "scary" recursion of the form type R<T> = {0: B, 1: R<F<T>>}[T extends U ? 0 : 1]), but it would be amazing if it were officially supported by the language.

All 29 comments

You can already do it:

interface FixedLengthArray<T extends any, L extends number> extends Array<T> {
    0: T;
    length: L;
}

type Foo = FixedLengthArray<number, 5>;
const foo1: Foo = [1, 2, 3, 4, 5];

At playground.

Oh I must have missed the trick with the 0 index. However it's not quite there, as keyof does not work like it does with tuples:

interface FixedLengthArray<T extends any, L extends number> extends Array<T> {
    "0": T;
    length: L;
}

type Foo = FixedLengthArray<number, 5>;
type Bar = [number, number, number, number, number];
type FooKey = Exclude<keyof Foo, keyof []>; // "0"
type BarKey = Exclude<keyof Bar, keyof []>; // "0" | "1" | "2" | "3" | "4"

@AlCalzone arrays can be sparse, tuples cannot:

const array = new Array(5)
const tuple = [1, 2, 3, 4, 5]

array.length === tuple.length // true

Object.keys(array) // []
Object.keys(tuple) // ["0", "1", "2", "3", "4"]

let arrayCount = 0
let tupleCount = 0

for (let i in array) { arrayCount++ }
for (let i in tuple) { tupleCount++ }

console.log(arrayCount, tupleCount); // 0 5

@jcready I know, but I think you missed my point. I'm asking for a way to define a large tuple type for a generic length that behaves just like the ones we can write out by hand - except without writing them out by hand. See my OP:

type T1 = number[3]; // equals [number, number, number]
type K1 = keyof T1; // 0 | 1 | 2

or maybe

type T1 = Tuple<number, 3>; // equals [number, number, number]
type K1 = keyof T1; // 0 | 1 | 2

type T2 = Tuple<number, 100>; // equals [number, number, ..., number] (100 items total)
type K2 = keyof T2; // 0 | 1 | ... | 99

in case this makes the intent clearer.

I edited the issue title to reflect this intent, as sparse arrays do actually work.

Some more context: It is currently possible to create a tuple of the type [N, N-1, ..., 0] using this code:

/**
 * Creates a union from the types of an Array or tuple
 */
type UnionOf<T extends any[]> = T[number];

/**
 * Returns the length of an array or tuple
 */
type LengthOf<T extends any[]> = T["length"];

/**
 * Returns all but the first item's type in a tuple/array
 */
export type Tail<T extends any[]> =
    ((...args: T) => any) extends ((head: any, ...tail: infer R) => any) ? R : never;

/**
 * Returns the given tuple/array with the item type prepended to it
 */
type Unshift<List extends any[], Item> =
    ((first: Item, ...rest: List) => any) extends ((...list: infer R) => any) ? R : never;

/**
 * Tests if two types are equal
 */
type Equals<T, S> =
    [T] extends [S] ? (
        [S] extends [T] ? true : false
    ) : false;

type Range<N, T extends number[] = []> = {
    0: T,
    1: Range<N, Unshift<T, LengthOf<T>>>,
}[Equals<LengthOf<Tail<T>>, N> extends true ? 0 : 1];

For example:

type T1 = Range<3>; // [3, 2, 1, 0]
type K1 = UnionOf<T1>; // 0 | 1 | 2 | 3

but this stops working for N > 98 and often freezes TypeScript while editing (#26155)

Those of us who want full-blown dependent types for tuples (for things like type-safe immutable tuple push/pop/concat/split/etc) should give this a big ๐Ÿ‘ .

Is there a canonical issue for dependent types for tuples? Probably needs something like one or more of:

  • compile-time number arithmetic (e.g., Add<2,2> ~ 4)
  • compile-time number comparison (e.g., LessThan<0,3> ~ true)
  • compile-time conversion between numeric string literals and number literals, (e.g., StrToNum<"0"> ~ 0)
  • compile-time specification of strict tuples of a given length (e.g., TupleLen<3, string> ~ [string, string, string]) โ† we are here
  • compile-time specification of open-ended tuples of a given prefix length (e.g., OpenTupleLen<1, string> ~ [string, ...string[]])
  • compile-time specification of (strict or open) tuples with optional elements starting at some index (e.g., TupleLenOptional<3, string, 1> ~ [string, string?, string?])

Most of the above can be faked up, at least for nonnegative integers less than some bound, via a bunch of hardcoded lookup types (or via the "scary" recursion of the form type R<T> = {0: B, 1: R<F<T>>}[T extends U ? 0 : 1]), but it would be amazing if it were officially supported by the language.

Assume for my answers that:

  • Tuple<T, 3> is equal to [T, T, T],
  • Concat<A, B> is equal to [...A, ...B]. Does not work yet, but can be done using two recursive types (very prone to breaking).
  • Range<N> returns a union 0 | 1 | ... | N

compile-time number arithmetic (e.g., Add<2,2> ~ 4)

LengthOf<Concat< Tuple<any, 2>, Tuple<any, 2> >>
Subtraction can probably done if mapped tuples support dropping values (#26190)

compile-time number comparison (e.g., LessThan<0,3> ~ true)

Works but suffers from the neccesity for recursive definition of Range<N>:

/** Tests if N > M */
type IsGreaterThan<N, M> = N extends Exclude<Range<N>, Range<M>> ? true : false;
/** Tests if N <= M */
type IsLessThanOrEqual<N, M> = Not<IsGreaterThan<N, M>>;
/** Tests if N < M */
type IsLessThan<N, M> = M extends Exclude<Range<M>, Range<N>> ? true : false;
/** Tests if N >= M */
type IsGreaterThanOrEqual<N, M> = Not<IsLessThan<N, M>>;

compile-time conversion between numeric string literals and number literals

Yes, please!

compile-time specification of open-ended tuples of a given prefix length
and: compile-time specification of (strict or open) tuples with optional elements starting at some index

Could be done by concatenation of a fixed-length tuple and an array (when that works): [...fixed, ...array] or [...fixed, ...optional]

well, here's my scary recursive tuple generator @AlCalzone: Vector.
It predated features like conditional types, so shinier (and more importantly, more robust) versions may be doable now.

Add<2,2> ~ 4 - LengthOf<Concat<Tuple<any, 2>, Tuple<any, 2>>>

That's pretty creative :smile:, if we had recursion-free Concat ([...T]) and Tuple then we'd have a recursion-free Add too!

others mentioned by @jcalz I recall hacky / legit implementations for:

  • LessThan
  • StrToNum
  • OpenTupleLen - #24897's "Optional elements in tuple types"
  • TupleLenOptional - #24897's "Rest elements in tuple types"

if we can make such generetics, then we will not be able to use

type Vec<T extends number> = T extends 1 ? [ number ] : T extends 2 ? [ number, number ] : number[];

const sum = <TLen>(a: Vec<TLen>, b: Vec<TLen>): Vec<TLen> => a.map((v, i) => v + b[i]);

const a: Vec<2> = [ 1, 2 ];
const b: Vec<2> = [ 1, 2 ];
const c = sum(a, b); // i expect c is Vec<2>... but ts think is Vec<number> // any number
const testZ =c[2]; // WTF!

You can already get the length of a tuple type T by querying T['length'], so I wouldn't recommend trying to infer it through a conditional type. So instead of

// try to infer L from Vec<L>, doesn't work ๐Ÿ™
declare const sum: <L extends number>(a: Vec<L>, b: Vec<L>) => Vec<L>;

I'd do something like

// instead infer V from a vector of type V and query its length to use instead of L
declare const sum: <V extends Vec<any>>(a: V, b: Vec<V['length']>) => Vec<V['length']>;

or in this particular case just

// All the lengths are the same so just use V
declare const sum: <V extends Vec<any>>(a: V, b: V) => V;

Of course we don't know how an "official" implementation of Vec or the like would behave; hopefully you'd be able to infer L from Vec<L> without any hoop-jumping.

The issue is not about inferring the tuple length but about specifying it

This tricks is dangerous bugable, ts is not support tuples in core, all tricks is bullshit.

export interface FixedLengthArray<T extends any, L extends number> extends Array<T> {
  '0': T;
  length: L;
}

export interface Vec<L extends number> extends FixedLengthArray<number, L>

const a: Vec<3> = [ 1, 2, 3 ];
const dd = b[999]; // bullshit!
b[1024]++; // bullshit!

a.push(2, 3, 4);
a.length === 6; // fuckup ha-ha is always false, no it is no solution

And one more problem

export interface FixedLengthArray<T extends any, L extends number> extends Array<T> {
  0: T;
  length: L;
}

let a: FixedLengthArray<number, 3> = [1, 2, 3];
const [x, y, z] = a; // Type 'FixedLengthArray<number, 3>' has no property '1'.

It is ironic for TypeScript to take so seriously for types and even template is created while length limited array types are still not supported:)

@AlCalzone In fact, if we have Tuple<T, Length>, we have Range<N>, Partial<Tuple<any, N>>['length'] will do the trick.

๐Ÿ˜ณ that's some next level shit.

I'm sure you all know how to implement the Tuple<T, Length> type but I thought I'd put it here for completeness sake

type PushFront<TailT extends any[], FrontT> = (
    ((front : FrontT, ...rest : TailT) => any) extends ((...tuple : infer TupleT) => any) ?
    TupleT :
    never
);

type Tuple<ElementT, LengthT extends number, OutputT extends any[] = []> = {
    0 : OutputT,
    1 : Tuple<ElementT, LengthT, PushFront<OutputT, ElementT>>
}[
    OutputT["length"] extends LengthT ?
    0 :
    1
];

//type t3 = [string, string, string]
type t3 = Tuple<string, 3>;
//type length = 0 | 3 | 1 | 2
type length = Partial<Tuple<any, 3>>['length'];

Playground


With the max instantiation depth, you can't get it to be too long, though.

type TupleImpl<ElementT, LengthT extends number, OutputT extends any[]> = {
    0 : OutputT,
    1 : TupleImpl<ElementT, LengthT, PushFront<OutputT, ElementT>>
}[
    number extends OutputT["length"] ?
    0 :
    OutputT["length"] extends LengthT ?
    0 :
    1
];
type Tuple<ElementT, LengthT extends number> = (
    TupleImpl<
        ElementT,
        LengthT,
        []
    > extends infer X ?
    (
        X extends any[] ?
        X :
        never
    ) :
    never
);

//type t41 = [string, ...39 more, string]
type t41 = Tuple<string, 41>;
/**
 * Expected : type t42 = [string, ...40 more, string]
 * Actual   : Type instantiation is excessively deep and possibly infinite.
 */
type t42 = Tuple<string, 42>;
/**
 * Expected : type t42impl = [string, ...40 more, string]
 * Actual   : type t42impl = [string, ...40 more, string]
 */
type t42impl = TupleImpl<string, 42, []>;

That pesky max instantiation depth rears its head again,

type PopFront<TupleT extends any[]> = (
    ((...tuple : TupleT) => void) extends ((head : any, ...tail : infer TailT) => void) ?
    TailT :
    never
);
type PushFront<TailT extends any[], FrontT> = (
    ((front : FrontT, ...tail : TailT) => void) extends ((...tuple : infer TupleT) => void) ?
    TupleT :
    never
);

/////////////////////////////////////////////////

type TupleImpl<ElementT, LengthT extends number, OutputT extends any[]> = {
    0 : OutputT,
    1 : TupleImpl<ElementT, LengthT, PushFront<OutputT, ElementT>>
}[
    number extends OutputT["length"] ?
    0 :
    OutputT["length"] extends LengthT ?
    0 :
    1
];
type Tuple<ElementT, LengthT extends number> = (
    TupleImpl<ElementT, LengthT, []> extends infer X ?
    (
        X extends any[] ?
        X :
        never
    ) :
    never
);

//type t3 = [string, string, string]
type t3 = Tuple<string, 3>;
//type length = 0 | 3 | 1 | 2
type length = Partial<Tuple<any, 3>>['length'];

type AddOne<N extends number> = (
    PushFront<Tuple<any, N>, any>["length"]
);
//type t41 = [string, ...39 more, string]
type t41 = Tuple<string, 41>;
/**
 * Expected : type t42 = [string, ...40 more, string]
 * Actual   : Type instantiation is excessively deep and possibly infinite.
 */
type t42 = Tuple<string, 42>;
/**
 * Expected : type t42impl = [string, ...40 more, string]
 * Actual   : type t42impl = [string, ...40 more, string]
 */
type t42impl = TupleImpl<string, 42, []>;
/**
 * Expected : type t42impl = [string, ...40 more, string]
 * Actual   : Type instantiation is excessively deep and possibly infinite.
 */
type t43impl = TupleImpl<string, 43, []>;


//4
type _3_plus_1 = AddOne<3>;
//5
type _4_plus_1 = AddOne<4>;
//10
type _9_plus_1 = AddOne<9>;

type SubOne<N extends number> = (
    PopFront<Tuple<any, N>>["length"]
);

//2
type _3_sub_1 = SubOne<3>;
//3
type _4_sub_1 = SubOne<4>;
//8
type _9_sub_1 = SubOne<9>;
/**
 * Expected: -1
 * Actual: 0
 * 
 * Because tuples cannot have length -1.
 * 
 * @todo Add signed-ness checks
 */
type _0_sub_1 = SubOne<0>;

type AddImpl<A extends number, B extends number> = {
    0 : B,
    1 : AddImpl<SubOne<A>, AddOne<B>>
}[
    A extends 0 ?
    0 :
    1
];
type Add<A extends number, B extends number> = (
    AddImpl<A, B> extends infer X ?
    (
        X extends number ?
        X :
        never
    ) :
    never
);
//15
type _7_plus_8 = AddImpl<7, 8>
/**
 * Excessively deep instantiation because we do not check for negative
 * 
 * @todo Add signed-ness checks
 */
type _neg1_plus_8 = AddImpl<-1, 8>
//21
type _19_plus_2 = Add<19, 2>
/**
 * Expected : 22
 * Actual   : Type instantiation is excessively deep and possibly infinite.
 */
type _20_plus_2 = Add<20, 2>

/**
 * Expected : 22
 * Actual   : number
 */
type _20_plus_2impl = AddImpl<20, 2>

Playground

๐Ÿš”๐Ÿšจ๐Ÿ‘ฎโ€โ™€๏ธ UH OH, IT'S THE SELF-APPOINTED CIRCULAR CONDITIONAL TYPE POLICE ๐Ÿ‘ฎโ€โ™‚๏ธ๐Ÿšจ๐Ÿš”

This style of construct: type A<T> = { 0: X, 1: A<B<T>>}[T extends Y ? 0 : 1] is not supported; if you use it in production code, do not be surprised if it gives unexpected or broken results in some environments... and any such code should probably feature prominent warnings to that effect.

The only official word I've seen on this is "don't do it" and "we're not ready for this". If recursive conditional types are ever officially supported in some form, I'd expect to see them included in the baseline tests for the language, so that future releases won't break them. Right now there are no such tests. Use recursive conditional types at your own risk, and inform downstream dependencies of said risk.

26980 seems to be the canonical place to talk about this.

๐Ÿš“๐Ÿ‘ฎโ€โ™€๏ธ THAT IS ALL ๐Ÿ‘ฎโ€โ™‚๏ธ๐Ÿš“

This are the two FixedLengthArray approaches i follow, depending of my needs :

The Tuple approach :

This solution provides a strict FixedLengthArray type signature based in Tuples.

Syntax example :

// Array containing 3 strings
let foo : FixedLengthArray<[string, string, string]> 

This is the safest approach, considering it prevents accessing indexes out of the boundaries.

Declaration :

type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
type FixedLengthArray<T extends any[]> =
  Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
  & { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }

Tests :

var myFixedLengthArray: FixedLengthArray< [string, string, string]>

// Array declaration tests
myFixedLengthArray = [ 'a', 'b', 'c' ]  // โœ… OK
myFixedLengthArray = [ 'a', 'b', 123 ]  // โœ… TYPE ERROR
myFixedLengthArray = [ 'a' ]            // โœ… LENGTH ERROR
myFixedLengthArray = [ 'a', 'b' ]       // โœ… LENGTH ERROR

// Index assignment tests 
myFixedLengthArray[1] = 'foo'           // โœ… OK
myFixedLengthArray[1000] = 'foo'        // โœ… INVALID INDEX ERROR

// Methods that mutate array length
myFixedLengthArray.push('foo')          // โœ… MISSING METHOD ERROR
myFixedLengthArray.pop()                // โœ… MISSING METHOD ERROR

// Direct length manipulation
myFixedLengthArray.length = 123         // โœ… READ-ONLY ERROR

// Destructuring
var [ a ] = myFixedLengthArray          // โœ… OK
var [ a, b ] = myFixedLengthArray       // โœ… OK
var [ a, b, c ] = myFixedLengthArray    // โœ… OK
var [ a, b, c, d ] = myFixedLengthArray // โœ… INVALID INDEX ERROR

(*) This solution requires the noImplicitAny configuration directive to be enabled in order to work


The Array(ish) approach :

This solution behaves as an augmentation of the Array type, accepting an additional second parameter(Array length). Is not as strict and safe as the Tuple based solution.

Syntax example :

// Array containing 3 strings
let foo: FixedLengthArray<string, 3> 

Keep in mind that this approach will not prevent you from accessing an index out of the declared boundaries and set a value on it.

Declaration :

type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' |  'unshift'
type FixedLengthArray<T, L extends number, TObj = [T, ...Array<T>]> =
  Pick<TObj, Exclude<keyof TObj, ArrayLengthMutationKeys>>
  & {
    readonly length: L 
    [ I : number ] : T
    [Symbol.iterator]: () => IterableIterator<T>   
  }

Tests :

```typescript
var myFixedLengthArray: FixedLengthArray

// Array declaration tests
myFixedLengthArray = [ 'a', 'b', 'c' ] // โœ… OK
myFixedLengthArray = [ 'a', 'b', 123 ] // โœ… TYPE ERROR
myFixedLengthArray = [ 'a' ] // โœ… LENGTH ERROR
myFixedLengthArray = [ 'a', 'b' ] // โœ… LENGTH ERROR

// Index assignment tests
myFixedLengthArray[1] = 'foo' // โœ… OK
myFixedLengthArray[1000] = 'foo' // โŒ SHOULD FAIL

// Methods that mutate array length
myFixedLengthArray.push('foo') // โœ… MISSING METHOD ERROR
myFixedLengthArray.pop() // โœ… MISSING METHOD ERROR

// Direct length manipulation
myFixedLengthArray.length = 123 // โœ… READ-ONLY ERROR

// Destructuring
var [ a ] = myFixedLengthArray // โœ… OK
var [ a, b ] = myFixedLengthArray // โœ… OK
var [ a, b, c ] = myFixedLengthArray // โœ… OK
var [ a, b, c, d ] = myFixedLengthArray // โŒ SHOULD FAIL
````

@SrBrahma if you want a type that enumerates 2 as its only possible value, you should declare it as a type.

type a = 2
let b: FixedLengthArray<number, a>

https://www.typescriptlang.org/play/?ssl=10&ssc=1&pln=11&pc=35#code/C4TwDgpgBAggTnAhiAMhAdgc2ACwLICuwiwAlgPboDSEIAzlALxQDkdYANqQMYQtQAfVmAJ0c-ISzDkwE1mNIAzYHNYF0C5SwBQoSFABipAB4QAJmiy54SEAB4AKgBooKKBGPAMZhugIBbACMIOBcHAHlAgCsmKABtZygAOhSbZEcAPgBdDKZtKCgABR4Aa0dIqJcAUWNuDgIzCDsS2nJFKAjolzTUDGx8IhIKalo6DIz8qAAyKABvSYK4CEQzSg4QKA4+3AAuVygF+KgASSg9vyCQqCyzjsO4gGUQIPIOJNIvJGByOCy9gAoAJRMXLHT6IQJbMEhEg-TIFA4FAC+2l04GgiFiACZtFtgFBAnsjKYLNscD07BdgqEoIgMkA

@colxi Your first approach with Array length parameter:

type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
type Tuple<T extends any[]> =
  Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
  & { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }

type FixedLengthArray<Type, Count extends number> =
  Count extends 1 ? Tuple<[Type]> :
  Count extends 2 ? Tuple<[Type, Type]> :
  Count extends 3 ? Tuple<[Type, Type, Type]> :
  // ...
  never;

Tests:

  var myFixedLengthArray: FixedLengthArray<string, 3>

  // Array declaration tests
  myFixedLengthArray = [ 'a', 'b', 'c' ]  // โœ… OK
  myFixedLengthArray = [ 'a', 'b', 123 ]  // โœ… TYPE ERROR
  myFixedLengthArray = [ 'a' ]            // โœ… LENGTH ERROR
  myFixedLengthArray = [ 'a', 'b' ]       // โœ… LENGTH ERROR

  // Index assignment tests 
  myFixedLengthArray[1] = 'foo'           // โœ… OK
  myFixedLengthArray[1000] = 'foo'        // โœ… INVALID INDEX ERROR

  // Methods that mutate array length
  myFixedLengthArray.push('foo')          // โœ… MISSING METHOD ERROR
  myFixedLengthArray.pop()                // โœ… MISSING METHOD ERROR

  // Direct length manipulation
  myFixedLengthArray.length = 123         // โœ… READ-ONLY ERROR

  // Destructuring
  var [ a ] = myFixedLengthArray          // โœ… OK
  var [ a, b ] = myFixedLengthArray       // โœ… OK
  var [ a, b, c ] = myFixedLengthArray    // โœ… OK
  var [ a, b, c, d ] = myFixedLengthArray // โœ… INVALID INDEX ERROR

What's the current status on this issue?
It would be pretty convenient to be able to specify a tuple's length just by doing,

const x: T[5] // -> [T, T, T, T, T]

or,

const x: Array<T, 5> // -> [T, T, T, T, T]

From PR ^

// Repeating tuples

type TupleOf<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;
type _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N ? R : _TupleOf<T, N, [T, ...R]>;

type T1 = TupleOf<string, 3>;  // [string, string, string]
type T2 = TupleOf<number, 0 | 2 | 4>;  // [] | [number, number] | [number, number, number, number]
type T3 = TupleOf<number, number>;  // number[]
type T4 = TupleOf<number, 100>;  // Depth error

Looks nice! Will wait [email protected] ๐Ÿคž

If anyone is morbidly curious for a way to achieve a greater depth limit, here's one that constructs tuples using a logarithmic approach instead of linear:

type BuildPowersOf2LengthArrays<N extends number, R extends never[][]> =
  R[0][N] extends never ? R : BuildPowersOf2LengthArrays<N, [[...R[0], ...R[0]], ...R]>;

type ConcatLargestUntilDone<N extends number, R extends never[][], B extends never[]> =
  B["length"] extends N ? B : [...R[0], ...B][N] extends never
    ? ConcatLargestUntilDone<N, R extends [R[0], ...infer U] ? U extends never[][] ? U : never : never, B>
    : ConcatLargestUntilDone<N, R extends [R[0], ...infer U] ? U extends never[][] ? U : never : never, [...R[0], ...B]>;

type Replace<R extends any[], T> = { [K in keyof R]: T }

type TupleOf<T, N extends number> = number extends N ? T[] : {
  [K in N]: 
  BuildPowersOf2LengthArrays<K, [[never]]> extends infer U ? U extends never[][]
  ? Replace<ConcatLargestUntilDone<K, U, []>, T> : never : never;
}[N]

It has no problems with tuples with lengths of thousands. It took a pretty long while to typecheck one of length 50,000. I didn't have the patience to see how long 100K would take. Edit: It was able to eventually typecheck 100K without getting a depth error.

This PR is friggin neat.
Here's another good use-case, typed dimensions for vectors and matrices:

type Vector<Length extends number> = TupleOf<number, Length>
type Matrix<Rows extends number, Columns extends number> = TupleOf<TupleOf<number, Columns>, Rows>

const v: Vector<2> = [1, 2]
const m: Matrix<2, 3> = [
  [1, 2, 3],
  [1, 2, 3],
]

Guys, could anyone help me understand this line?

type TupleOf<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;

Specifically, N extends N. What could be the case when N does not extend N?
I changed it to simpler

type TupleOf<T, N extends number> = number extends N ? T[] : _TupleOf<T, N, []>;

and it works just as well on the provided test cases. Here is the playground link.

@aigoncharov The N extends N ? xxx : never conditional ensures that xxx is distributed over N when N is a union type. Specifically, instead of evaluating xxx once when N is a union type, xxx is evaluated for each individual constituent of N and those evaluations are then unioned together. For more on distributive conditional types, see #21316.

Your modified type doesn't work for the following test case:

type T2 = TupleOf<number, 0 | 2 | 4>;  // Expected [] | [number, number] | [number, number, number, number]

The result of this test case changes to [] if you remove the distributive conditional.

@ahejlsberg thank you! TS is wild. Cool, but still wild :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bgrieder picture bgrieder  ยท  3Comments

Zlatkovsky picture Zlatkovsky  ยท  3Comments

siddjain picture siddjain  ยท  3Comments

kyasbal-1994 picture kyasbal-1994  ยท  3Comments

manekinekko picture manekinekko  ยท  3Comments