【问题标题】:Changing Property Name in Typescript Mapped Type在 Typescript 映射类型中更改属性名称
【发布时间】:2020-12-31 12:25:31
【问题描述】:

我有一组如下所示的 Typescript 对象:

SomeData {
  prop1: string;
  prop2: number;
}

我需要得到一些看起来像这样的对象:

type SomeMoreData= {
  prop1Change: string;
  prop2Change: number;
}

我知道如果我只是想改变类型,我可以这样做:

type SomeMoreData<T> = {
  [P in keyof T]: boolean;
}

这会给我:

SomeMoreData<SomeData> {
  prop1: boolean;
  prop2: boolean;
}

有没有办法操纵[P in keyof T] 位产生的名称?我尝试过[(P in keyof T) + "Change"] 之类的方法,但没有成功。

【问题讨论】:

  • 循环遍历 SomeData[] 并填充 SomeMoreData[] 是一种选择。
  • 这不会给我类型验证问题吗?我试图避免在那里有一个“任何”对象。

标签: typescript


【解决方案1】:

TS4.1 之前的答案;

您无法自动执行此操作。最大的障碍是当前有 no type operator 允许您在类型级别附加字符串文字,因此您甚至无法描述您正在执行的转换:

// without Append<A extends string, B extends string>, you can't type this:

function appendChange<T extends string>(originalKey: T): Append<T,'Change'> {
  return originalKey+'Change';
}

此功能有一个suggestion,但谁知道它是否会发生。

这意味着如果你想转换键,你实际上需要硬编码你正在寻找的特定映射,从字符串文字到字符串文字:

type SomeMoreDataMapping = {
  prop1: "prop1Change"
  prop2: "prop2Change"
}

有了这个映射,你可以定义这些:

type ValueOf<T> = T[keyof T]
type KeyValueTupleToObject<T extends [keyof any, any]> = {
  [K in T[0]]: Extract<T, [K, any]>[1]
}
type MapKeys<T, M extends Record<string, string>> =
  KeyValueTupleToObject<ValueOf<{
    [K in keyof T]: [K extends keyof M ? M[K] : K, T[K]]
  }>>

简要介绍:

  • ValueOf&lt;T&gt; 只返回类型 T 的属性值类型的联合。
  • KeyValueTupleToObject 采用如下元组类型的联合:["a",string] | ["b",number] 并将它们转换为如下对象类型:{a: string, b: number}
  • MapKeys&lt;T, M&gt; 采用T 类型和键映射M 并将T 中的任何键替换为M 中存在的任何键与来自M 的相应键。如果T 中的键在M 中不存在,则不会转换该键。如果M 中的某个键在T 中不存在,它将被忽略。

现在你(终于)可以这样做了:

type SomeMoreData= MapKeys<SomeData, SomeMoreDataMapping>;

如果你检查SomeMoreData,你会发现它的类型是正确的:

var someMoreData: SomeMoreData = {
  prop1Change: 'Mystery Science Theater',
  prop2Change: 3000
} // type checks

这应该可以让你做一些有趣的事情,比如:

function makeTheChange<T>(input: T): MapKeys<T, SomeMoreDataMapping> {
  var ret = {} as MapKeys<T, SomeMoreDataMapping>;
  for (var k in input) {
    // lots of any needed here; hard to convince the type system you're doing the right thing
    var nk: keyof typeof ret = <any>((k === 'prop1') ? 'prop1Change' : (k === 'prop2') ? 'prop2Change' : k);
    ret[nk] = <any>input[k];    
  }
  return ret;
}

var changed = makeTheChange({ prop1: 'Gypsy', prop2: 'Tom', prop3: 'Crow' });
console.log(changed.prop1Change.charAt(0)); //ok
console.log(changed.prop2Change.charAt(0)); //ok
console.log(changed.prop3.charAt(0)); //ok

希望对您有所帮助。祝你好运!

2018 年 9 月更新以利用 TS 2.8 引入的 conditional types 的优势


另一个更新:如果您希望它与可选属性一起使用,它会变得更加复杂:

type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T];
type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T];
type MapKeys<T, M extends Record<string, string>> =
  KeyValueTupleToObject<ValueOf<{
    [K in RequiredKeys<T>]-?: [K extends keyof M ? M[K] : K, T[K]]
  }>> & Partial<KeyValueTupleToObject<ValueOf<{
    [K in OptionalKeys<T>]-?: [K extends keyof M ? M[K] : K, T[K]]
  }>>> extends infer O ? { [K in keyof O]: O[K] } : never;

如果您需要支持索引签名,它会变得更加复杂。

Playground link to code

【讨论】:

  • 如果您事先不知道该属性的名称,并且您想为每个属性添加另一个不同类型的属性,该怎么办:{ prop1: number } -> { prop1: number, prop1Observable: Observable }。基本上,你可以在没有反向映射的情况下做同样的事情吗?如果我理解正确,你不能。
  • @jcalz 是否可以使用可选的 props 进行这项工作:例如,当 SomeDataprop1 是可选的 - prop1?: string; 映射类型 SomeMoreData 为空时
  • 是的,你可以做到,只是更复杂。我会编辑答案
【解决方案2】:

映射类型中的键重映射为introduced in typescript 4.1

type SomeData = {
    prop1: string;
    prop2: number;
}

type WithChange<T> = { [P in keyof T & string as `${P}Change`]: T[P] };

type SomeMoreData = WithChange<SomeData>; // {  prop1Change: string, prop2Change: number }

Playground


在上面的示例中,我们使用了两个新功能:映射类型中的 as 子句 + template literal type

模板字符串类型是模板字符串表达式的类型空间等价物。与模板字符串表达式类似,模板字符串类型包含在反引号分隔符中,并且可以包含${T} 形式的占位符,其中T 是可分配给stringnumberboolean 或@ 的类型987654330@。模板字符串类型提供连接文字字符串、将非字符串原始类型的文字转换为其字符串表示形式以及更改字符串文字的大小写或大小写的能力。此外,通过类型推断,模板字符串类型提供了一种简单的字符串模式匹配和分解形式。

一些例子:

type EventName<T extends string> = `${T}Changed`;
type Concat<S1 extends string, S2 extends string> = `${S1}${S2}`;
type T0 = EventName<'foo'>;  // 'fooChanged'
type T1 = EventName<'foo' | 'bar' | 'baz'>;  // 'fooChanged' | 'barChanged' | 'bazChanged'
type T2 = Concat<'Hello', 'World'>;  // 'HelloWorld'
type T3 = `${'top' | 'bottom'}-${'left' | 'right'}`;  // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'

附加映射类型支持可选的as 子句,通过该子句可以指定生成的属性名称的转换:

{ [P in K as N]: X }

其中N 必须是可分配给string 的类型。

【讨论】:

  • 我猜既然 ahejlsberg 写了 PR,它可能会被合并...等不及要在 typescript@next 中了!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-21
  • 2016-05-22
  • 2018-08-30
相关资源
最近更新 更多