好的,我感谢到目前为止的 comments ,让我更具体地重新定义这个问题.

我有以下设置:

type AbstractIntlMessages = {
    [id: string]: AbstractIntlMessages | string;
};

interface Translations<Locales extends string, Messages extends AbstractIntlMessages = AbstractIntlMessages> {
  Messages: Record<Locales, () => Promise<{ default: Messages }>>;
}

async function combineTranslations<
  Locales extends string,
  Messages extends AbstractIntlMessages = AbstractIntlMessages
>(locale: Locales, definitions: Array<Translations<Locales>>): Promise<Messages> {
  const imported = await Promise.all(definitions.map((definition) => definition.Messages[locale]()));
  const translations = imported.map((t) => t.default);
  return Object.assign({}, ...translations);
}

const SiteTranslations = {
  Messages: {
    en_US: () => Promise.resolve({ default: { Site: { title: 'My Site' } } }),
    es_US: () => Promise.resolve({ default: { Site: { title: 'Mi Sitio' } } }),
  }
};

const AuthFeatureTranslations = {
  Messages: {
    en_US: () => Promise.resolve({ default: { Auth: { title: 'My Auth' } } }),
    es_US: () => Promise.resolve({ default: { Auth: { title: 'Mi Autentico' } } }),
  }
};

const locales = ['en_US', 'es_US'] as const;
type Locales = (typeof locales)[number];

async function i18n(locale: Locales) {
  const messages = await combineTranslations(locale, [
    SiteTranslations,
    AuthFeatureTranslations
  ]);

  console.log(messages)
}

i18n('en_US')

运行combineTranslations后,messages的类型是AbstractIntlMessages.

How do I structure the types of 100 such that the type of 101 is:

{
  Site: {
    title: string;
  };
  Auth: {
    title: string;
  };
}

我正在使用next-intl,它使用messages的类型来驱动开发期间的自动完成和类型验证.目前这不起作用,因为messages的类型是AbstractIntlMessages.

我还要注意,messages的属性是否只读并不重要.此外,messages的叶属性可以是string,也可以是翻译中的实际字符串值.

以下是TSplayground 的链接,代码为:https://tsplay.dev/w8qj4N

推荐答案

您可以给予combineTranslations()以下呼叫签名:

async function combineTranslations<
    L extends Locales,
    const M extends AbstractIntlMessages[]
>(
    locale: L,
    definitions: { [I in keyof M]: Translations<L, M[I]> }
): Promise<{ [I in keyof M]: (x: M[I]) => void } extends
    { [k: number]: (x: infer U) => void } ? { [P in keyof U]: U[P] } : never
> {
    const imported = await Promise.all(definitions.map(
      (definition) => definition.Messages[locale]()));
    const translations = imported.map((t) => t.default);
    return Object.assign({}, ...translations);
}

这是两个类型参数中的generic:L,对应于locale参数的类型; M,对应于与definition数组相关的AbstractIntlMessages个子类型的array. 因此,如果您调用combineTranslations("en_US", [SiteTranslations, AuthFeatureTranslations]),我们预计L将是"en_US"M将是[{ Site: { title: string }}, { Auth: { title: string }}].

请注意,definitions的类型并不是直接的M,而是形式{ [I in keyof M]: Translations<L, M[I]> }M上的mapped array/tuple type(参见What does "homomorphic mapped type" mean?). 这意味着,对于(indexed access type M[I]的)M数组的类数字索引I处的每个元素,definitions数组的相应元素将属于类型Translations<L, M[I]>. TypScript可以从同形映射类型中推断,因此目的是M可以从definitions推断.

哦,请注意,M是一个const type parameter,以暗示编译器我们希望将M推断为tuple,而不是无序array.

一旦我们有了LM,那么返回类型是Promise<{ [I in keyof M]: (x: M[I]) => void } extends { [k: number]: (x: infer U) => void } ? { [P in keyof U]: U[P] } : never>,本质上是Promise<TupleToIntersection<M>>,其中TupleToIntersection是假设的实用类型,它接受像[X, Y, Z]这样的二元组并将其转换为像X & Y & Z这样的intersection.它使用了与Transform union type to intersection type中所示类似的技术.因此,如果M[{ Site: { title: string }}, { Auth: { title: string }}],那么输出类型应该是Promise<{ Site: { title: string }} & { Auth: { title: string }}>,这相当于所需的Promise<{ Site: { title: string }; Auth: { title: string }}>类型,其中交集已被"拉平"为单个对象类型. 如果有关系,推断出的U类型是M的元素的交集,而映射的类型{ [P in keyof U]: U[P] }U的"拉平"版本.


好吧,让我们测试一下:

const messages = await combineTranslations(locale, [
    SiteTranslations,
    AuthFeatureTranslations
]);
/* function combineTranslations<
     "en_US" | "es_US", 
     [{ Site: { title: string; };}, { Auth: { title: string; };}]
   > */
messages.Auth.title.toUpperCase();
messages.Site.title.toLowerCase();

看起来不错messages的类型就是想要的{ Site: { title: string }, Auth: { title: string }}.

Playground link to code

Typescript相关问答推荐

如何修复VueJS中的val类型不能赋给函数

脉轮ui打字脚本强制执行图标按钮的咏叹调标签-如何关闭

需要使用相同组件重新加载路由变更数据—React TSX

在TypeScript中使用泛型的灵活返回值类型

我们过go 不能从TypeScript中的联合中同时访问所有属性?

React typescribe Jest调用函数

基于键的值的打字动态类型;种类;

如何在TypeScrip中使用嵌套对象中的类型映射?

STypescript 件的标引签名与功能签名的区别

TypeScrip-将<;A、B、C和>之一转换为联合A|B|C

等待用户在Angular 函数中输入

React Typescript项目问题有Redux-Toolkit userSlice角色问题

是否可以针对联合类型验证类型属性名称?

如何使所有可选字段在打印脚本中都是必填字段,但不能多或少?

是否将自动导入样式从`~/目录/文件`改为`@/目录/文件`?

try 呈现应用程序获取-错误:动作必须是普通对象.使用自定义中间件进行MySQL操作

如何在不违反挂钩规则的情况下动态更改显示内容(带有状态)?

编剧- Select 下拉框

如何在TypeScript类成员函数中使用筛选后的keyof this?

元素隐式具有any类型,因为string类型的表达式不能用于索引类型{Categories: Element;管理员:元素; }'