嵌套对象的打字稿keyof

时间:2021-04-01 07:16:03

标签: typescript

我有以下类型

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

我想定义另一种基于该组构建的类型

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

然后我定义对象

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

问题是打字稿在 prop 字段上抱怨。 groupName 字段很好。

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

我不知道如何解决这个问题。

2 个答案:

答案 0 :(得分:2)

问题

考虑以下代码-

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

type Z = Groups[keyof Groups]

type X = keyof Z

Playground

类型 Z 是来自 Groups 的所有可能类型的联合。联合类型的 keyof 返回 never。这就是为什么 propAllGroups 的类型是 never

解决方案

我们从 Groups 创建联合类型,然后将它们用作 Discriminated Unions

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']
  },
];

Playground

答案 1 :(得分:2)

考虑下一个例子:

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

type Keys = keyof Result // never

上述联合中没有通用密钥,因此 TS 无法确定应返回给您的密钥。这就是您收到错误消息的原因。

要处理此类联合,您可以使用@Shivam Singla 的方式或这种通用方式:

// 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>

所以,而不是写:

keyof Groups[keyof Groups];

你可以写:

keyof StrictUnion<Groups[keyof Groups]>

这是我的完整解决方案:

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

Playground

缺点:在这里你应该使用除了类型推断之外什么都不做的函数。

我认为它会被 V8 引擎优化。

您也可以使用 closure webpack plugin 来摆脱函数调用。

但是,我投票支持 @Shivam Singla 的解决方案,因为没有任何功能开销。

我刚刚发布了这个解决方案,为您提供了一些替代方法。

这种类型有点复杂:

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;

但这就是我验证函数参数的方式。

如果你对解释感兴趣,请ping我,我会更新答案