skip to Main Content

I’ve got this interface which will serve as props for a section component displaying different equipment. Each piece of equipment will be placed on a card inside a grid column.
Here’s the interface:

interface EquipmentSectionProps {
  bgColor: "black" | "lightGrey";
  columns: number;
  category: string;
  products: string[];
  productImages: string[];
  productReferals: string[];
}

Given the property columns, I want to restrict the size of the arrays products, productImages and productReferals with the property columns since their disposal is correlated with the number of columns.

The obvious first thing to try was to pass the property column inside the array directly like this:

interface EquipmentSectionProps {
  bgColor: "black" | "lightGrey";
  columns: number;
  category: string;
  products: string[columns];
  productImages: string[columns];
  productReferals: string[columns];
}

however, the response comes back as

Cannot find name 'columns'.ts(2304) type columns = /*unresolved*/ any

Is there an easier way to make this possible without creating a duplicated interface and passing the property into the new one?

2

Answers


  1. Unfortunately you can’t restrict the length of an array via Typescript types (e.g. interface declarations like the one you’re showing). You’ll have to write the length checks inside the code for your component.

    Even though there’s a way to initialize an array with a specific length in javascript, there’s no enforcement within javascript that can make the length immutable. Nothing is stopping the array from being lengthened, which means you need to check the length explicitly as-needed.

    Login or Signup to reply.
  2. You can make the interface generic in the type of the columns property, and then define the corresponding array properties to be tuple types of the appropriate length.

    In order to do this you’d need a utility type like TupLen<N extends number, V> which will resolve to a tuple of length N where every element is of type V:

    interface EquipmentSectionProps<N extends number> {
      bgColor: "black" | "lightGrey";
      columns: N;
      category: string;
      products: TupLen<N, string>;
      productImages: TupLen<N, string>;
      productReferals: TupLen<N, string>;
    }
    
    const good: EquipmentSectionProps<3> = {
      bgColor: "black", category: "abc", columns: 3,
      products: ["a", "b", "c"],
      productImages: ["d", "e", "f"],
      productReferals: ["g", "h", "i"]
    };
    
    const bad: EquipmentSectionProps<3> = {
      bgColor: "black", category: "abc", columns: 3,
      products: ["a", "b", "c"],
      productImages: ["d", "e"], // error! 2 elements but requires 3
      productReferals: ["g", "h", "i", "j"] // error! 4 elements but only allows 3.
    };
    

    But there’s no built in TupLen<N, V>; you have to write your own. One way is as a tail-recursive conditional type using variadic tuple types:

    type TupLen<N extends number, V = any, A extends V[] = []> =
        number extends N ? V[] : N extends A['length'] ? A : TupLen<N, V, [...A, V]>;
    
    type TestNumber4 = TupLen<4, number>;
    // type TestNumber4 = [number, number, number, number]
    type TestBoolean2 = TupLen<2, boolean>;
    // type TestBoolean2 = [boolean, boolean]
    

    Also, if you don’t want to have to write the column length twice, as in, const v: EquipmentSectionProps<3> = { columns: 3, ⋯} (once for the type argument and once for the columns property), you could instead write a helper function to infer the generic type argument from the value of the columns property:

    const equipmentSectionProps = 
      <N extends number>(esp: EquipmentSectionProps<N>) => esp;
    
    const good = equipmentSectionProps({
      bgColor: "black", category: "abc",
      columns: 3,
      products: ["a", "b", "c"],
      productImages: ["d", "e", "f"],
      productReferals: ["g", "h", "i"]
    })
    // const good: EquipmentSectionProps<3>
    
    const bad = equipmentSectionProps({
      bgColor: "black", category: "abc",
      columns: 3,
      products: ["a", "b", "c"],
      productImages: ["d", "e"], // error, 
      // '[string, string]' is not '[string, string, string]'.
      productReferals: ["g", "h", "i", "j"] // error, 
      // '[string, string, string, string]' is not '[string, string, string]'.
    })
    // const good: EquipmentSectionProps<3>
    

    You get the same behavior without needing to explicitly write out EquipmentSectionProps<3>.

    Playground link to code

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search