Home > front end >  How to create a generic object type that uses the key as a specific value instead of type string wit
How to create a generic object type that uses the key as a specific value instead of type string wit

Time:09-04

Is it possible to create a generic object type that would force the keys to be used as values internally?

For example, if I want an object to have the id that matches the key I would use something like the code below:

export type ObjectWithId<
  T extends string,
  E extends Record<string, unknown>
> = {
  [K in T]: { id: K } & E;
};

type IdKeys = 'dog' | 'cat';

const animals: ObjectWithId<IdKeys, { title: string }> = {
  dog: { id: 'dog', title: 'Some title' },
  cat: { id: 'cat', title: 'Another title' },
};

But I wonder if there is a solution that could work without the need to declare the IdKeys and therefore would infer these values from the object.

e.g.

const animals: ObjectWithId<{ title: string }> = {
  dog: { id: 'dog', title: 'Some title' },
  cat: { id: 'cat', title: 'Another title' },
};

CodePudding user response:

You need to create extra builder function in order to make this type validation:

const foo = <
    Id extends PropertyKey,
    Value extends Record<string, unknown>,
    >(a: Record<Id, { id: Id } & Value>) => a


// ok
foo({
    dog: { id: 'dog', title: 'Some title' },
    cat: { id: 'cat', title: 'Another title' },
})

// error
foo({
    dog: { id: 'cat', title: 'Some title' }, 
    cat: { id: 'cat', title: 'Another title' },
})

Above approach is good, but it might be even better. As you might have noticed, if id is wrong, the whole row is highlighted.

Consider this example:


type ErrorMessage<T extends string> = `ID property should be equal to ${T}`

type ValidateId<Obj extends Record<PropertyKey, { id: PropertyKey }>> = {
    /**
     * Iterate throught eahc Key
     */
    [Key in keyof Obj]:
    /**
     * Check whether [id] property is equal to Key
     */
    Obj[Key]['id'] extends Key
    /**
     * If yes, leave object as it is
     */
    ? Obj[Key]
    /**
     * Itherwise, replace [id] value with readable error message
     */
    : Omit<Obj[Key], 'id'> & {
        id: ErrorMessage<Key & string>
    }
}
const foo = <
    Id extends PropertyKey,
    Value extends Record<string, unknown>,
    Data extends Record<Id, { id: Id } & Value>
>(a: ValidateId<Data>) => a

foo({
    dog: { id: 'dog2', title: 'Some title' }, // only ID property is highlighted
    cat: { id: 'cat', title: 'Another title' },
})

Playground

Now, only id property is highlighted with nice error message.

If you are interested in this approach, you can check my articles: Type inference on function arguments and Validators

P.S. If you don't like using function and still want to achieve what you want, my answer is - no. It is impossible in typescript generate such constraint without knowing upfront your keys

  • Related