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
]
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
]}
/>
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"'.