Búsqueda de sitios web

Typescript keyof de objeto anidado


Estoy teniendo el siguiente tipo

export type Groups = {
  group1: {
    prop1?: string;
    prop2?: number;
  };
  group2: {
    prop3?: string;
  };
};

Quiero definir otro tipo que se construya basado en ese grupo

type AllGroups = {
  groupName: keyof Groups;
  prop: keyof Groups[keyof Groups];
  values: string[];
}[];

Entonces defino el objeto

const allGroups: AllGroups = [
  {
    groupName: 'group1',
    prop: 'prop1',
    values: ['a']
  },
];

El problema es que TypeScript está dando errores en el campo prop. El campo groupName está bien.

TS2322: Type 'string' is not assignable to type 'never'. The expected type comes from property 'prop''

No estoy seguro de cómo abordar esto.

Respuesta #1

Considera el siguiente ejemplo:

type Result = {
    prop1?: string | undefined;
    prop2?: number | undefined;
} | {
    prop3?: string | undefined;
}

type Keys = keyof Result // never

No hay claves comunes en la unión anterior, por lo tanto TypeScript no puede determinar qué claves debe devolverte. Por eso estás recibiendo un error.

Para manejar este tipo de uniones, puedes usar la manera de @Shivam Singla o esta manera genérica:

// credits goes to Titian Cernicova-Dragomir 
//https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

Entonces, en lugar de escribir:

keyof Groups[keyof Groups];

Puedes escribir:

keyof StrictUnion<Groups[keyof Groups]>

Aquí está mi solución completa:

export type Groups = {
    group1: {
        prop1?: string;
        prop2?: number;
    };
    group2: {
        prop3?: string;
    };
};

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>


interface AllGroups {
    groupName: keyof Groups;
    prop: keyof StrictUnion<Groups[keyof Groups]>;
    values: string[];
};

const allGroups = <T extends AllGroups, U extends {
    0: ReadonlyArray<T>,
}[
    T['groupName'] extends keyof Groups
    ? T['prop'] extends keyof Groups[T['groupName']]
    ? 0 : never : never]>(arr: ReadonlyArray<T> & U) => arr;

const result = allGroups(
    [{
        groupName: 'group1',
        prop: 'prop2',
        values: ['a']
    },
    {
        groupName: 'group1',
        prop: 'prop1',
        values: ['a']
    }]) // ok

const result2 = allGroups(
    [{
        groupName: 'group1',
        prop: 'prop2',
        values: ['a']
    },
    {
        groupName: 'group1',
        prop: 'prop3', // expected error
        values: ['a']
    }]) // ok

Parque infantil

Desventajas: aquí deberías usar una función que literalmente no haga nada excepto inferir tipos.

Supongo que será optimizado por el motor V8.

También puedes usar el complemento closure de webpack para deshacerte de la llamada a la función.

Sin embargo, estoy votando por la solución de @Shivam Singla porque no hay sobrecarga de funciones.

Acabo de publicar esta solución para darte una forma alternativa.

Este tipo es un poco complicado:

const allGroups = <T extends AllGroups, U extends {
    0: ReadonlyArray<T>,
}[
    T['groupName'] extends keyof Groups
    ? T['prop'] extends keyof Groups[T['groupName']]
    ? 0 : never : never]>(arr: ReadonlyArray<T> & U) => arr;

Pero esta es la forma en que hago mi validación de los argumentos de la función.

Respuesta #2

Problema

Considere el siguiente código-

export type Groups = {
  group1: {
    prop1?: string;
    prop2?: number;
  };
  group2: {
    prop3?: string;
  };
};

type Z = Groups[keyof Groups]

type X = keyof Z

Parque infantil

El tipo Z es una unión de todos los tipos posibles de Groups. El keyof de un tipo unión devuelve never. Por eso el tipo de prop en AllGroups era never

Solución

Creamos un tipo unión a partir de Groups y luego los usamos como Uniones Discriminadas

export type Groups = {
  group1: {
    prop1?: string;
    prop2?: number;
  };
  group2: {
    prop3?: string;
  };
};

type AllGroups = {
  [K in keyof Groups]: {
    groupName: K
    prop: keyof Groups[K]
    values: string[]
  }
}[keyof Groups][]

const allGroups: AllGroups = [
  {
    groupName: 'group1',
    prop: 'prop2',
    values: ['a']
  },
  {
    groupName: 'group1',
    prop: 'prop3', // error expected
    values: ['a']
  },
  {
    groupName: 'group2',
    prop: 'prop3',
    values: ['a']
  },
  {
    groupName: 'group2',
    prop: 'prop1', // error expected
    values: ['a']
  },
];

Parque infantil