I'm trying to build a react component which shows a stack images, something like this, and for that, I'm trying to type it with interface and types to enforce taking some props in a particular way. Here's my interface code from before:
old-interface.ts
export interface ImageStackProps{
images: string[];
backgroundColor: string;
extraBannerBackgroundColor: string;
imageSize: ImageSize<string>;
imagesToShow: number;
direction?: StackDirection<"column" | "row" | "column-reverse" | "row-reverse">;
}
type StackDirection<T> = T | Array<T | null> | { [key: string]: T | null };
type ImageSize<T> = { width: T; height: T };
The imagesToShow property decides, how many images it's going to show and if it'll show the extra images remaining banner or not. Although it's not necessary, I was trying to constraint imagesToShow property based of the images property (which gets an array of base64 encoded images), so that a value larger than the last index of the array is never allowed. So, for that, I modified my interface to look something like this:
new-interface.ts
export interface ImageStackProps<ImagesArray extends Array<string>> {
images: ImagesArray;
backgroundColor: string;
extraBannerBackgroundColor: string;
imageSize: ImageSize<string>;
imagesToShow: AllowedArrayIndexes<ImagesArray>;
direction?: StackDirection<"column" | "row" | "column-reverse" | "row-reverse">;
}
type StackDirection<T> = T | Array<T | null> | { [key: string]: T | null };
type ImageSize<T> = { width: T; height: T };
type ArrayKeys<T extends Array<string>> = keyof T;
type AllowedArrayIndexes<T extends Array<string>> = Exclude<ArrayKeys<T>, ArrayKeys<string[]>>;
But the issue at this point that I'm facing is that to type my ImagesStack
component, I need to pass a type to ImageStackProps
, which is something that my ImagesStack
components receive in its props.images
.
I understand and know that it's not necessary to have this constraint and it works just fine without it, but I'm looking to see if I can achieve this. I'm pretty sure that I'm not approaching this correctly. Is there a better way to enforce this? Or better, is this even achievable somehow? How should I modify my interface?
But the issue at this point that I'm facing is that to type my ImagesStack component, I need to pass a type to ImageStackProps, which is something that my ImagesStack components receive in its props.images.
This is the easy part. You just make the component generic and pass that generic type parameter to the props type.
function MyComponent<T extends readonly string[]>(props: ImageStackProps<T>) {
return <></>
}
But the rest of your solution has some problems.
AllowedArrayIndexes
returns string keys ("0" | "1" | "2"
), not numbers (0 | 1 | 2
).AllowedArrayIndexes
return indices, which is one less than should allowed. A 3 item array has indices 0 | 1 | 2
but you should be allowed to display 3 images.I propose a different approach.
First, we need way to get number
indices from a tuple. This issue has some neat methods for that. But let's go with this:
type AllowedCounts<T extends { length: number }> = Partial<T>["length"];
This looks like voodoo, but let's walk through it.
A tuple has a known length. [number,number,number]
has a length
of 3
. But Partial<T>
makes all members optional, so:
Partial<[number, number, number]>
// yields
[number?, number?, number?]
This means the tuple might have missing members, and then the length would be less. All of the following types are assignable to [number?, number?, number?]
:
[] // length 0
[number] // length 1
[number, number] // length 2
[number, number, number] // length 3
So:
Partial<[number, number, number]>['length'] // 0 | 1 | 2 | 3
You could even exclude 0
if you want, since showing an image stack with zero images doesn't make a lot of sense.
Exclude<Partial<[number, number, number]>['length'], 0> // 1 | 2 3
This works nicely for unbounded array types as well, since:
type A = string[]['length'] // number
type B = Partial<string[]>['length'] // number
Although you cannot exclude 0
from this case, since typescript does not have negative types:
Exclude<number, 0> // number, sadly.
Now putting that to use is as simple as:
export interface ImageStackProps<ImagesArray extends readonly string[]> {
images: ImagesArray;
imagesToShow: AllowedCounts<ImagesArray>;
}
type AllowedCounts<T extends { length: number }> = Partial<T>["length"];
// A component
function MyComponent<T extends readonly string[]>(props: ImageStackProps<T>) {
return <></>
}
// With known tuple length at compile time
const imagesTuple = ['a.jpg', 'b.jpg'] as const // 2 images, component can show
const jsxA = <MyComponent images={imagesTuple} imagesToShow={0} />
const jsxB = <MyComponent images={imagesTuple} imagesToShow={1} />
const jsxC = <MyComponent images={imagesTuple} imagesToShow={2} />
const jsxD = <MyComponent images={imagesTuple} imagesToShow={3} /> // error
const jsxE = <MyComponent images={imagesTuple} imagesToShow={50} /> // error
// With unknown array length at compile time
declare const imagesArr: string[] // from an api, for example.
const jsxX = <MyComponent images={imagesArr} imagesToShow={1} /> // fine
const jsxY = <MyComponent images={imagesArr} imagesToShow={123456} /> // fine