简体   繁体   中英

Infer a type key from another object's values

I have a interface column.

interface column {
  field: string
  label: string
}

So a row of columns would be:

const cols = [{
    label: 'First Name',
    field: 'fname',
  },
  {
    label: 'Last Name',
    field: 'lname',
  },
  {
    label: 'Email',
    field: 'email',
  }]

Data would be an array of data rows.

const data:{}[] = [{
fname: 'bob', lname: 'dylan', email: 'db@email.com',
fname: 'van', lname: 'halen', email: 'vh@email.com'
}]

Is there a way to enforce keys ie [fname, lname, email] of data to be the values of corresponding cols[n]['field']?? Thanks in advance.

Yes, you can do this as long as you define cols in such a way so that the compiler doesn't widen the field properties to string . The easiest way to do that with TS3.4+ is to use a const assertion :

const cols = [{
  label: 'First Name',
  field: 'fname',
},
{
  label: 'Last Name',
  field: 'lname',
},
{
  label: 'Email',
  field: 'email',
}] as const;

That as const means that cols will be considered a readonly tuple where the label and field properties have string literal types.

From there you can make a type alias that converts a set of Column -compatible values to an object type with the field properties as keys. Of course there's no mention of what the value type of each field should be, so I'm assuming it's always string :

interface Column {
  field: string
  label: string
};

type StringObjFromColumns<T extends readonly Column[]> =
  { [K in T[number]["field"]]: string }

And then we can define your data type as StringObjFromColumns<typeof cols> :

const data: StringObjFromColumns<typeof cols>[] = [{
  fname: 'bob', lname: 'dylan', email: 'db@email.com',
}, {
  fname: 'van', lname: 'halen', email: 'vh@email.com'
}, {
  fname: 'van', lname: 'morrison', age: 75, email: 'vm@example.com' // error! age is extra
}, {
  fname: 'ted', lname: 'nugent' // error! email is missing
}]

You can see how it enforces the constraint that each object must have those three fields of type string .

Okay, hope that helps; good luck!

Playground link to code

This is not possible as typescript types are enforced at compile time and not run time. See my answer to this question which answers yours.

This is a tricky one.
Having

interface Column {
    field: string
    label: string
}

We can define a RowObject and, additionally, just for the sake of example, a Row which has the column property and the row itself.

interface RowObject {
    [key: string]: string;
}

interface Row {
    cols: Column[];
    object: RowObject;
}

Now, we should define a function to prototype the new objects.

const createRowBlueprint = (cols: Column[]): Row => {
    return {
        cols: [...cols],
        object: cols.map(c => c.field)
                    .reduce((prop: RowObject, prev: string) => (prop[prev] = '', prop), {})
    };
};

I just defined the Row interface to dynamically access the "real row object" properties, like so

const row = createRowBlueprint(cols);
row.cols.forEach(col => {
    const { field } = col;
    const value = row.object[field];
    console.log(`Field ${field} binded to ${value}`);
});

Given your columns collection, this outputs the following

{
  cols: [
    { label: 'First Name', field: 'fname' },
    { label: 'Last Name', field: 'lname' },
    { label: 'Email', field: 'email' }
  ],
  row: { fname: '', lname: '', email: '' }
}

Field fname binded to
Field lname binded to
Field email binded to

Obviously, I set the default value to an empty string. But, the prove that it works is that the value accessed from the field is not undefined .

Finally, this approach is not safety typed. But, as far as I know, Typescript does not support dynamic type definition in compilaiton time, for the moment... So this is a tricky workaround.

Hope it helps.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM