Search code examples
typescripttypescript-genericstype-constraints

Typescript: Is it possible to ensure the key and an inner value are the same string?


I have a Record of data that could become large. I'm wondering if is it possible to enforce the key of the Record to be the same as the inner name value?.

interface Person<T> {
  name: T
  title: string
  description: string
}

type People = Record<string, Person<string>>

// example data

const data: People = {
  sarah: {
    name: 'sarah',
    title: 'Sarah',
    description: 'Hello',
  }
}

const badData: People = {
  adam: {
    name: 'john', // This would cause a typescript error ideally.
    ...
  }
}

I've tried setting up People with a generic but it would require me to add all the keys to a union type which I would rather avoid. keyof doesn't work as the object isn't defined where the keyof is needed.

type People<T> = Record<T, Person<T>>

const people: People<keyof typeof people> = {} // Block-scoped variable 'people' used before its declaration.ts(2448)

Solution

  • In order to do that, you need to use extra function.

    interface Person<T> {
        name: T
        title: string
        description: string
    }
    
    type People = Record<string, Person<string>>
    
    
    type Validate<T extends People> = {
        [Name in keyof T]: Name extends T[Name]['name'] ? T[Name] : T[Name] & { name: never }
    }
    
    
    const validator = <
        Name extends string,
        Human extends Person<Name>,
        Data extends Record<Name, Human>
    >(data: Validate<Data>) => data
    
    const result = validator({
        sarah: {
            name: 'sarah',
            title: 'Sarah',
            description: 'Hello',
        },
        adam: {
            name: 'john', // Error.
            title: 'Sarah',
            description: 'Hello',
        }
    })
    

    Playground

    Validate iterates over each object key/Name and checks whether top level name is equal Object[Name]['name']. If yes - return same nested object. If no - return same nested object but with iverriden name proeprty never.

    Hence, you are getting error in a place which should be fixed.

    If you are interested in Type Inference on function arguments you can check my article

    More generic version of Validation from @Konrad Madej:

    type Validate<Data extends People> = {
        [Name in keyof Data]: Name extends Data[Name]['name'] ? Data[Name] : Omit<Data[Name], 'name'> & { name: Name }
    }
    

    If you have numeric keys, please consider this example:

    interface PayloadObj<T> {
        elementId: T;
        values: any;
    }
    
    type Payload = Record<number, PayloadObj<number>>;
    
    type Validate<T extends Payload> = {
        [Key in keyof T]: T[Key] & { elementId: Key }
    };
    
    const validator = <
        ElementId extends number,
        PayloadValue extends PayloadObj<ElementId>,
        Data extends Record<ElementId, PayloadValue>
    >(data: Validate<Data> & Data): Payload => data;
    
    const result = validator({
        0: {
            elementId: 0,
            values: 'Hello',
        },
        1: {
            elementId: 2, // Error.
            values: 'World',
        },
    });
    

    Playground