type _Tuple< | |
T, | |
N extends number, | |
R extends readonly T[] = [] | |
> = R["length"] extends N ? R : _Tuple<T, N, readonly [T, ...R]>; | |
type Tuple<T, N extends number> = _Tuple<T, N> & { | |
readonly length: N; | |
[I: number]: T; | |
[Symbol.iterator]: () => IterableIterator<T>; | |
}; | |
function zip< | |
A extends readonly any[], | |
Length extends A["length"], | |
B extends Tuple<any, Length> | |
>(a: A, b: B): Tuple<readonly [A[number], B[number]], Length> { | |
if (a.length !== b.length) { | |
throw new Error(`zip cannot operate on different length arrays; ${a.length} !== ${b.length}`); | |
} | |
return a.map((v, index) => [v, b[index]]) as Tuple< | |
readonly [A[number], B[number]], | |
Length | |
>; | |
} | |
const a = [1, 2, 3] as const; | |
const b1 = [1, 2, 6, 2, 4] as const; | |
const b2 = [1, 2, 6] as const; | |
// @ts-expect-error Source has 5 element(s) but target allows only 3. | |
const c1 = zip(a, b1); | |
const c2 = zip(a, b2); | |
console.log(c2); | |
// ^? |
I was able to take this helper and create an assertsHasLength
:
type _Tuple<
T,
N extends number,
R extends readonly T[] = []
> = R["length"] extends N ? R : _Tuple<T, N, readonly [T, ...R]>;
type Tuple<T, N extends number> = _Tuple<T, N> & {
readonly length: N;
[I: number]: T;
[Symbol.iterator]: () => IterableIterator<T>;
};
function zip<
A extends readonly any[],
Length extends A["length"],
B extends Tuple<any, Length>
>(a: A, b: B): Tuple<readonly [A[number], B[number]], Length> {
if (a.length !== b.length) {
throw new Error(
`zip cannot operate on different length arrays; ${a.length} !== ${b.length}`
);
}
return a.map((v, index) => [v, b[index]]) as Tuple<
readonly [A[number], B[number]],
Length
>;
}
// ...
type TupleSeq_<T, L, A extends [...any]> = A["length"] extends L
? A
: TupleSeq_<T, L, [...A, T]>;
type TupleSeq<T, L> = TupleSeq_<T, L, []>;
function assertsHasLength<T, L extends number>(
arr: T[],
length: L
): asserts arr is TupleSeq<T, L> {
if (arr.length !== length) {
throw new Error(
`Array has a length of ${arr.length} instead of ${length}.`
);
}
}
declare const a: string[];
declare const b: number[];
assertsHasLength(a, 3);
assertsHasLength(b, 3);
const [first, second, third] = a;
const arr = zip(a, b);
It's not as helpful as we'd like, since (1) our Tuple
type resolves number
into an empty tuple instead of an array of a non-fixed length, and (2) rather than prescribing that an array has a length of a particular numeric literal, we want to be able to specify that an array has a Length
which is the same as the length of another array.
I wonder if the latter is possible already or whether it could be a feature request that the TypeScript team might be able to implement...
Rather than using length
in this case, could we make use of opaque/branded types?
See: https://twitter.com/sebinsua/status/1583064897432477696?s=61&t=Fb9i9QQ8Pp-4n0iSmZ05CA
Unfortunately, regarding not matching on number
, it doesn't seem possible to use extends
in this way as by definition all numeric literals inherit from number
. I guess we'd need TypeScript to add a NumberLiteral
type that is the superset of all numeric literals in between individual numeric literals and number
. (Maybe this type could just be an infinite number interval.)
And, regarding the possibility of using a runtime assertion of equal lengths (or some other property) to assign an opaque/branded/flavoured type to two or more values, this isn't currently possible as TypeScript doesn't support multiple/aggregate function assertions or propagation of type assertions outside of their scope.
I've been wondering whether it was possible to create a type-safe
zip
operation and did so today.Although, maybe this isn't how people normally use
zip
, recently I've been thinking about how in Python you join series/frames and end up withNaN
ornull
s in columns which are empty, and how this can cause bugs downstream. I was wondering whether it'd be possible to make a type-safe data programming language... Basically, by having invariants on things like lengths, and only letting people ignore these if they provide a function to "impute" missing values, etc.