Home > Software engineering >  Typing a prop based on the provided values for another prop
Typing a prop based on the provided values for another prop

Time:09-29

So i'm trying to figure out how to make the properties of an interface match the provided values for another interface.

e.g

interface IHeader: Array<{header: string, label: string}> 
interface IData: Array<{ /** props are values of header **/}>

//for example if the header is:
const columns: IHeader = [
  {header: "age", label: "Age"},
  {header:"name", label: "Name"},
  {header: "date", label: "Date"},
]
// Then the available props on data should be:
const data: IData = [
  {age: "24", name: "some guy", date: "monday"}, // OK - all props match the columns.header values
  {age: "24", name: "some guy"}, // ERROR: missing 'date'
  {age: "24", name: "some guy", date: "monday", location: "1234 street"} // ERROR: location is not a valid property
]

How would i go about writing a type definition that takes all the values of 'header' provided across the entire array and creates a property name for each unique one? or is this even possible

Edit: digging deeper into the actual implementation i'm going for

the suggestion in the comments below worked when 'columns' is defined, however i'm trying to get this typing to apply to props passed to a component. here's the implementation I have so far

Page.ts

export default function Page() {
return (
<Table
  columns={[
    {label: "name", ...otherProps}
    {label: "date", ...}
  ]}
  data:={[
    {name: "me", date: "monday"}
    {name: "me", date: "tuesday", location: "1234 some place"} // Does not throw compiler error for location: invalid prop
  ]}
)}

Table.ts

type Header = ReadonlyArray<{label: string, align: string}> 
type Data<T extends Header> = ReadonlyArray<Record<T[number]['label'], string>>
/**
* interface TableProps extends DefaultTableProps {
*  columns: Header,
*  data: Data<typeof columns> // Obviously wrong, but don't know what to put. compiler says 'cannot find columns' understandably
*}
**/
export default function Table(props: {columns: Header, data: Data<typeof columns>}) { // no compiler error but props are obviously not set right because Page.ts does not complain about erroneous props
  const headerColumns = props.headerColumns
  const data: Data<typeof headerColumns> = props.data
// other code, like setting a default orderBy for the table
  const [orderBy, setOrderBy] = useState<keyof Data<typeof headerColumns>>(dataKeys[0] as keyof Data<typeof headerColumns>);
return (
  {...table stuff}
)}

Ideally i would like to be able to define an interface that contains the correct typings for columns and data, and give it to the function export because i need to extend some default props, but i can settle for manually typing the object

export default function Table(props: TableProps) {...} // contains columns, data, default props defined in an interface elsewhere, and still warns the compiler when there's extra props in the data object-array

CodePudding user response:

To extract the exact type information from columns, you'll need to modify the definition a bit, as the original colums has type {header: string, label: string}[], which does not contain the actual header values anymore:

const columns = [
  { header: 'age', label: 'Age' },
  { header: 'name', label: 'Name' },
]
// const columns: {header: string, label: string}[]

One way to handle this is by narrowing the type of each object by declaring it read-only with as const:

const columns = [
  { header: 'age', label: 'Age' } as const,
  { header: 'name', label: 'Name' } as const,
]
// const columns: ({readonly header: "age", readonly label: "Age"} | {readonly header: "name", readonly label: "Name"})...

This preserves the literal type information, but is a bit awkward as each element needs a separate as const, so the easiest solution is to make the whole array read-only:

const columns = [
  { header: 'age', label: 'Age' },
  { header: 'name', label: 'Name' },
  { header: 'date', label: 'Date' },
] as const
// const columns: readonly [{readonly header: "age", readonly label: "Age"}, {readonly header: "name", readonly label: "...

This also turns it into a tuple type, but that's not relevant here as we're only interested in the union of the element types.

From columns, we can obtain the type of allowed keys by indexing with number (to get a union of array-element types) and then indexing with 'header'

type ColumnKeys = typeof columns[number]['header']
// type ColumnKeys = "age" | "name" | "date"

We can do this generically and create an array of string records with:

type Header = ReadonlyArray<{ header: string; label: string }>
type Data<T extends Header> = ReadonlyArray<Record<T[number]['header'], string>>

Note that Header needs to be read-only, as columns will be a read-only array. Data doesn't strictly need to be read-only, but read-only arrays provide more type safety, and allow the type to be used for both mutable and read-only arrays, whereas a mutable array type cannot be use for a read-only array.

Using Data, we can now use Data<typeof columns> to get errors in all the right places:

type ColumnData = Data<typeof columns>
const data: ColumnData = [
  {age: "24", name: "some guy", date: "monday"}, // OK
  {age: "24", name: "some guy"}, // ERROR: Property 'date' is missing in type
  {age: "24", name: "some guy", date: "monday", location: "1234 street"}
                                                // ERROR: 'location' does not exist in type
]

TypeScript playground

It's also possible to use Data in React, but you will need to make the Table component generic:

function Table<H extends Header>(props: { columns: H, data: Data<H> }) {
  return <div></div>
}

and make sure to use as const for the columns prop:

<Table
  columns={
    [
      { label: 'name', align: 'left' },
      { label: 'date', align: 'left' },
    ] as const
  }
  data={[
    { name: 'me', date: 'monday' },
    { name: 'me', date: 'tuesday', location: '1234 some place' },
                                   // ERROR
  ]}
/>

TypeScript playground

Small update: Accidentally omitting as const in the columns prop will disable key type checking for data. If this turns out to be problematic, you could consider using a workaround like this:

type Data<T extends Header> =
  T extends Array<unknown>
  ? "ERROR: please use 'as const' in columns prop"
  : ReadonlyArray<Record<T[number]['label'], string>>

It's not ideal, but it will trigger an error at data if columns is a mutable array:

Type '({ name: string; date: string; } | { name: string;
date: string; location: string; })[]' is not assignable to type
'"ERROR: please use 'as const' in columns prop"'.

TypeScript playground

  • Related