TypeScript:类型重命名键函数

jyztefdp  于 2022-12-19  发布在  TypeScript
关注(0)|答案(1)|浏览(261)

我有一个函数,它接受一个重命名函数和一个对象,并根据重命名函数重命名对象的键。

export default function mapKeys<T extends { [s: string]: T } | ArrayLike<T>>(
  renameFunction: (key: string) => string,
  object: T,
): T {
  return Object.fromEntries(
    Object.entries(object).map(([key, value]) => [renameFunction(key), value]),
  );
}

您可以像这样使用它:

mapKeys(key => key.toUpperCase(), { a: 1, b: 2 });
// => { A: 1, B: 2 }

问题是,TypeScript抱怨这两个,函数定义:

Type '{ [k: string]: T; }' is not assignable to type 'T'.
  '{ [k: string]: T; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ [s: string]: T; } | ArrayLike<T>'.

以及应用程序的错误:

Type 'number' is not assignable to type '{ a: number; b: number; }'.

以及

Type 'number' is not assignable to type '{ a: number; b: number; }'.

有没有方法可以输入这个函数,从而编译这些错误?理想情况下,TypeScript还应该知道函数的输出形状,例如,如果renameFunction类似于:

const rename = (a: string) => `${a}Error`;

其目的是:

{ email: 'foo' }

然后TypeScript知道,结果对象中的键是emailError
我试着把这个函数写得更一般一些,如下所示:

export default function mapKeys(
  renameFunction: (key: string) => string,
  object: object,
): object {
  return Object.fromEntries(
    Object.entries(object).map(([key, value]) => [renameFunction(key), value]),
  );
}

这将编译,但TypeScript将丢失有关对象键的信息。
如果我有一个像{ name: 'foo' }这样的对象和一个像rename: (a: string) => a + 'Foo'这样的重命名函数,TypeScript不知道返回的对象的形状是{ nameFoo: string }

vpfxa7rd

vpfxa7rd1#

在这种情况下,你需要定义两个东西,对于每个renameFunction,实际的(运行时)实现和类型级实现。当然,你会受到类型系统的限制,所以你不能在类型系统中表示任何任意的运行时Map。
因此,renameFunction将具有以下类型:

type RenameFunction<T extends Renamer> = (x: string) => string

其中T将是类型级函数,右侧是运行时实现。
Renamer,类型级函数实现的一部分,实现如下:

interface Renamer {
    A: string
    type: string
}

其中A表示输入类型,type表示输出类型。
然后,这将是实际类型级函数的形状:

interface UppercaseRenamer extends Renamer {
    type: Uppercase<this["A"]>
}

type将是A字段的结果,其中某个类型级函数被“调用”。
这是类型级函数声明,你还需要另一面,你调用这个类型级函数的地方。

type HigherKind<F extends Renamer, A> = (F & { A: A })["type"]

在这里,我们合并了表示类型级函数的F和表示参数的{A: A},并从表示类型级结果的"type"字段中提取,这样,我们就在typescript中模拟了更高级的类型。
然后,您可以在实际函数中使用该编码:

type Result<T, U extends Renamer> = 
{
    [K in keyof T as HigherKind<U, K>] : T[K]
}

export default function mapKeys<T extends object, U extends Renamer>(
  renameFunction: RenameFunction<U>,
  object: T,
): Result<T, U> {
  return Object.fromEntries(
    Object.entries(object).map(([key, value]) => [renameFunction(key), value]),
  ) as Result<T, U>
}

最后一部分是运行时实现,您需要在签名中指明类型级函数。

const uppercase: RenameFunction<UppercaseRenamer> = (x: string) => x.toUpperCase()

为了他的召唤:

const result = mapKeys(uppercase, {a: "ttt", c: "xxx"})

result的类型为:

{
  A: string,
  C: string
}

您还可以定义其他函数,如下所示:

interface RepeatRenamer extends Renamer {
    type: `${this["A"]}${this["A"]}`
}
const repeat: RenameFunction<RepeatRenamer> = (x: string) => `${x}${x}`
const result2 = mapKeys(repeat, {a: "b", c: "d"})

那么,result类型将为

{
  aa: string,
  cc: string
}

相关问题