我在用图书馆https://github.com/ivanhofer/typesafe-i18n

这个库生成强类型的翻译信息和函数,如下所示.(以下是简化的例子)

export type MyTranslations = {
  Hello: (arg: { field: unknown}) => string
  Bye: (arg: { field: unknown, date: unknown}) => string
  Foo: (arg: { field: unknown}) => unknown
  Bar: (arg: { max: unknown}) => unknown,
  test: string // this is just here to show that not every property of MyTranslations needs to be a function
}

const translations: MyTranslations = {
  Hello: (arg: { field: unknown}) => 'hello',
  Bye: (arg: { field: unknown, date: unknown}) => 'bye',
  Foo: (arg: { field: unknown}) => 'foo',
  Bar: (arg: { max: unknown}) => 'bar',
  test: '' // this is just here to show that not every property of MyTranslations needs to be a function
}

Now in my code I have a function which should translate messages dynamically, it does not know exactly what I has to translate.
Through TS typing information it knows what I might translate (with keyof).
Here is the code so far.
I spend already quite some time and I'm not sure if it is even possible or sensible, but I just want to know :)

// preparation
interface MyParams {
  [index: string]: boolean | number | string | undefined
  field?: keyof MyTranslations
}

interface Result {
  transKey: keyof MyTranslations,
  params?: MyParams
}

const results: Result[] = [
  {
    transKey: 'Hello',
    params: {
      field: 'Bye'
    }
  },
  {
    transKey: 'Bar',
    params: {
      max: 'test'
    }
  }
] 

type PickByType<T, V> = {
  [P in keyof T as T[P] extends V | undefined ? P : never]: T[P]
}

翻译功能

function translate(results: Result[]) {
  results.forEach((result: Result) => {
      type A = PickByType<MyTranslations, Function>
      type C = keyof A
     
      if(result.params) {
        type T = typeof result.params
        type Req = Required<T>

        const req = result.params as Req
        
        const func = translations[result.transKey]
        type F = typeof func
        

        const f = translations as A
        f[result.transKey as C](req)

      }
  })
}

translate(results)

问题在这里f[result.transKey as C](req)

Error

Argument of type 'Required<MyParams>' is not assignable to parameter of type '{ field: unknown; } & { field: unknown; date: unknown; } & { field: unknown; } & { max: unknown; }'.
  Property 'date' is missing in type 'Required<MyParams>' but required in type '{ field: unknown; date: unknown; }'

Which makes sense. Typescript expects an intersection type.
So I thought maybe I can create this type somehow (holding all the required parameters field, max and date and then, according to this type information create a new object of this new type holding this information, like so in pseudo code

type D = getAllParametersFromTypeAsIntersectionType() // <- this is easy
const newParams = createNewParamsAsTypeD(result.params)

有什么 idea 吗?

TS Playground

推荐答案

你不会真的想把result.params当成intersection type.首先,它不是一个:它需要有每一个属性(例如,{field: ⋯, date: ⋯, max: ⋯}),但实际上,您只传递translations[result.transKey]为特定result.transKey所需的属性. TypeScript期望和交集的原因是它不知道result.transKeyresult.params之间预期的高阶关系. 你的Result类型实际上并没有编码任何这样的关系(你可以写{ transKey: 'Hello', params: { max: 'Bye' } },它会被接受,即使这不是Hello的正确类型).即使你把它编码为每transKey个可接受类型的union,它也不会在forEach()回调中自动工作,因为TypeScript不能很好地处理"相关联合".

microsoft/TypeScript#30581份报告涵盖了对相关unions 缺乏直接支持的情况.推荐的方法是重构以特定方式使用generics,如microsoft/TypeScript#47109所述.

这个 idea 是编写一个"基"对象类型,它代表了你关心的底层键值关系,然后你的所有操作都应该使用该类型,泛型indexes into该类型,并在该类型上使用泛型索引到mapped types.

你的基对象类型是

interface TransArg {
    Hello: { field: unknown; };
    Bye: { field: unknown; date: unknown; };
    Foo: { field: unknown; };
    Bar: { max: unknown; };
}

实际上,你可以从MyTranslations计算这个值,如下所示:

type TransKey = {
  [K in keyof MyTranslations]: MyTranslations[K] extends (arg: any) => any ? K : never
}[keyof MyTranslations]
// type TransKey = "Hello" | "Bye" | "Foo" | "Bar"

type TransArg = { [K in TransKey]: Parameters<MyTranslations[K]>[0] }

TransKey的东西本质上是keyof PickByType<MyTranslations, Function>在你的版本.请注意,这一切都只是为了避免test键,这是一种从你的主要问题转移注意力,但它很容易克服,所以这很好.

然后TransArg映射到TransKey上,以获取方法的参数类型. 现在我们需要用TransArg重写translations的类型,如下所示:

const _translations: { [K in TransKey]: (arg: TransArg[K]) => void } =
  translations;

除了验证translations是否属于映射类型之外,这并没有真正做任何事情,但是现在我们可以使用_translations来代替translations,编译器将能够更好地遵循它对任意键K所做的事情,因为它是显式编码在类型中的(而不是MyTranslations,它只有隐式的信息).

我们现在可以更准确地将Result写成distributive object type(正如ms/TS#47109中创造的那样):

type Result<K extends TransKey = TransKey> =
  { [P in K]: { transKey: P, params?: TransArg[P] } }[K]

因此,对于特定的KResult<K>正好是适合于该transKeyResult类型. Result<TransKey>TransKey中每KResult<K>的完全联合. default type argumentTransKey意味着只要写Result就能给我们完整的联盟. 现在你可以写

const results: Result[] = [
  {
    transKey: 'Hello',
    params: {
      field: 'Bye'
    }
  },
  {
    transKey: 'Bar',
    params: {
      max: 'test'
    }
  }
]

如果你试图混淆params在那里(例如,{max: 'test'}'Hello')你会得到一个错误.

我们快结束了.现在我们可以用generic回调函数调用results.forEach():

function translate(results: Result[]) {
  results.forEach(<K extends TransKey>(result: Result<K>) => {
    if (result.params) _translations[result.transKey](result.params);
  })
}

在回调中,_translations[result.transKey]是单个泛型类型(arg: TransArg[K]) => void,而result.params的类型是TransArg[K](好吧,它是TransArg[K] | undefined,但我们已经通过判断if (result.params)消除了undefined). 这样你就有了一个函数类型,它接受一个和我们传递的参数完全对应的参数,所以编译起来没有问题.

Playground link to code

Typescript相关问答推荐

具有映射返回类型的通用设置实用程序

学习Angular :无法读取未定义的属性(正在读取推送)

如何根据特定操作隐藏按钮

使用TypeScrip根据(可选)属性推断结果类型

如何在Angular 12中创建DisplayBlock组件?

TypeScrip原始字符串没有方法

如何在TypeScrip中正确键入元素和事件目标?

Cypress页面对象模型模式.扩展Elements属性

具有自引用属性的接口

TypeScrip泛型类未提供预期的类型错误

正交摄影机正在将渲染的场景切成两半

如何从一个泛型类型推断多个类型

如何扩展RxJS?

如何将通用接口的键正确设置为接口的键

从以下内容之一提取属性类型

是否可以定义引用另一行中的类型参数的类型?

在Vite中将SVG导入为ReactComponent-不明确的间接导出:ReactComponent

如何知道使用TypeScript时是否在临时工作流内运行

使用嵌套属性和动态执行时,Typescript 给出交集而不是并集

是否可以从 TypeScript 的类型推断中排除this?