rena.to logo image
·
short
·

TypeScript Tuple literal length

Cool tricks you can do with it

Tuples have such a cool and underrated feature that their `length` property is actually the literal length number, not only just a `number` type.
type A = [any, any]["length"];
// ^? type A = 2
type B = [any, any, any]["length"];
// ^? type B = 3
I want to explore some tricks that it casually enable to.

# Literal string length

I was a bit disappointed when I found out that this doesn’t work the same way with a string literal, given the fact that, theoretically, TypeScript also knows exactly what its length is.
type A = `some`['length']
// ^? type A = number
But you can still get it. The trick here is you can create a tuple from a literal string by splitting each char.
To do that you need to rely on a behavior where extending a literal string always infer the first character when it's composed with more than one infer.
type A = `some` extends `${infer I}` ? [I] : never;
// ^? type A = ["some"]
type B = `some` extends `${infer I1}${infer I2}` ? [I1, I2] : never;
// ^? type B = ["s", "ome"]
type C = `some` extends `${infer I1}${infer I2}${infer I3}` ? [I1, I2, I3] : never;
// ^? type C = ["s", "o", "me"]
Knowing that, you can recursively split every character into a brand new tuple.
type S2T<S> = S extends `${infer C}${infer R}` ? [C, ...S2T<R>] : [];
type T = S2T<"some">;
// ^? type T = ["s", "o", "m", "e"]
You can now have the length of a literal string:
type A = S2T<"some">["length"]
// ^? type A = 4
Whether this is useful or not depends on your creativity. Imagine you have a function that accepts an arbitrary string but it to be of a fixed length
type LengthOf<N, S> = S2T<S>["length"] extends N ? S : never;
declare function fn<T extends string>(str: LengthOf<4, T>): any;
const a = fn("foo") // error: Argument of type '"foo"' is not assignable to parameter of type 'never';
const b = fn("test") // ok

# Arithmetic addition

The trick here starts with the fact you can also create a tuple from a literal number.
By being able to check if a tuple have the length you actual desire e.g. `T["length"] extends 4`, you can use a tail to recursively build a new n-length tuple you want:
type N2T<N, T extends any[] = []> = T["length"] extends N ? T : N2T<N, [...T, any]>;
type T = N2T<5>;
// ^? type A = [any, any, any, any, any]
And because you can join tuples together
type A = [...[any, any], ...[any]]
// ^? type A = [any, any, any]
You actually also can do some arithmetic addition using only typescript type-system, effortless:
type Add<A, B> = [...N2T<A>, ...N2T<B>]["length"]
type V = Add<3, 5>
// ^? type V = 8
Again, whether this is useful or not depends on your creativity.
But at the moment, I'm not worried about that. It's just cool as fuck!
Note
Unfortunately, the larger the number, the more expensive it is for TypeScript because of the recursion when creating tuples. So it can only add numbers up to 999 as it identifies that the recursion would be deep or infinite and break out of it.
So `Add<999, 999>` becomes `1998` but `Add<1000, 1>` becomes `number`
As a bonus, here's an Eval implementation that parses from a string, ignore whitespaces, and fits in a tweet:
type R<T> = T extends `${infer A} ${infer B}` ? `${A}${R<B>}` : T;
type O<N, T extends ""[] = []> = T["length"] extends N ? T : O<N, [...T, ""]>;
type P<T> = T extends `${infer A extends number}+${infer B extends number}` ? [...O<A>, ...O<B>]["length"] : 0;
type Eval<T> = P<R<T>>;
type A = Eval<"34 + 83">
// ^? type A = 117
type B = Eval<"34+83">
// ^? type B = 117
type C = Eval<" 34 + 83 ">
// ^? type C = 117
Follow me on twitter, and github. Leave a comment.