如何有条件地获取数组中所有对象的键的并集?

How to conditionally get a union of keys of all objects in an array?

假设我有以下内容:

const common = {
  "test": "Test",
  "test2": "Test2"
}

const greetings = {
  "hello": "Hello"
}

export const locales = (["en", "nl"] as const);
export type I18nMap = Record<typeof locales[number], {
    common: typeof common;
    greetings: typeof greetings;
}>;

我希望能够 select 像 "common" 这样的单个键或像 ["common", "greetings"] 这样的键数组。基于 selected 键,我希望能够 select selected 对象的任何键,我不确定如何实现,下面是我的尝试

type Namespace = keyof I18nMap[keyof I18nMap];

export function useTranslation<
  T extends Namespace | Namespace[],
  U extends T extends Namespace
    ? keyof I18nMap[keyof I18nMap][Namespace]
    : keyof I18nMap[keyof I18nMap][Namespace][number]
>(namespace: T) {}

当 namspace 为“common”时,我希望只有 "test" | "test2" 可用于类型 U,当我选择数组 [“common”,“greetings”] 时,我希望联合 "test" | "test2" | "hello" 对于类型 U

首先,让我们将 {common: typeof common; greetings: typeof greetings} 类型移动到界面中,以便更容易使用:

interface I18nData {
  common: typeof common;
  greetings: typeof greetings;
}

export type I18nMap = Record<typeof locales[number], I18nData>;

type Namespace = keyof I18nData;

T 应该只是扩展 Namespace(而不是 Namespace | Namespace[])并且 namespace 参数应该是 T | T[]。当 namespace 是包含不同类型的数组时,TypeScript 将推断 T 为这些类型的并集。

那么,U 应该扩展 keyof I18nData[T]。然而,这样做是行不通的:

export function useTranslation<
  T extends Namespace,
  U extends keyof I18nData[T]
>(namespace: T | T[], key: U) {}
// Argument of type 'string' is not assignable to parameter of type 'never'.
useTranslation(['common', 'greetings'], 'test');

这是因为 T'common' | 'greetings'I18nData[T] 变成了 {test1: string; test2: string} | {hello: string}。因为联合类型之间没有公共键,所以 keyof I18ndata[T] 变成 never.

要解决此问题,您可以使用分布式条件类型:

export function useTranslation<
  T extends Namespace,
  // For every T, get keyof I18nData[T]
  U extends T extends unknown ? keyof I18nData[T] : never
>(namespace: T | T[], key: U) {}

// Works
useTranslation('common', 'test');
useTranslation('greetings', 'hello');
useTranslation(['common', 'greetings'], 'test');
useTranslation(['common', 'greetings'], 'hello');


// Fails
useTranslation('common', 'hello');
useTranslation('greetings', 'test');
useTranslation(['common'], 'hello');

Playground link