一般复制和重命名属性



我正在尝试将现有类型作为新类型的基础,该类型有以下目标:

  • 包含原始和重命名的属性
  • 如果原始属性是必需的,则需要原始属性或重命名属性(类似于以下内容)
  • 如果原始属性是可选的,重命名的属性也是可选的

我已经找到了一种通用的方法来重命名属性,但是我很难弄清楚是否有一种通用的方法来满足其他标准。

我有:

export type Postfix<K extends string, T extends string> = `${T}${K}`;
export type Postfixer<K, T extends string> = {
[P in keyof K as Postfix<T, string & P>]: K[P];
};
type Foo = {
one: string;
two: string;
three: string;
}
type DuplicatedAndRenamed = Postfixer<Foo, ".$">;
// works already
const foo: DuplicatedAndRenamed = {
"one.$": 'foo',
"three.$": 'bar',
"two.$": 'baz',
}
// goal which should work
const shouldWork: DuplicatedAndRenamed = {
"one.$": 'foo',
three: 'bar',
"two.$": 'baz',
}
// goal which should not work
const shouldNotWork: DuplicatedAndRenamed = {
"one.$": 'foo',
three: 'bar',
"three.$": 'bar',
"two.$": 'baz',
}

想知道这是否可能?

下面的解决方案有点冗长。我相信有人会找到一个更短的解决方案:)

export type Postfix<K extends string, T extends string> = `${T}${K}`;
type ExpandRecursively<T> = T extends object
? T extends infer O ? { [K in keyof O]: ExpandRecursively<O[K]> } : never
: T;
type UnionToIntersection<U> = 
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
type Postfixer<T, K extends string> = ExpandRecursively<
UnionToIntersection<
{
[P in keyof T]-?: [
| XOR<{ [Key in Postfix<K, string & P>]: T[P] }, { [Key in P]: T[P] }>
| (undefined extends T[P]
? { [Key in Postfix<K, string & P> | P]: undefined }
: never)
];
}[keyof T]
>
>[any];
type Foo = {
one: string;
two: string;
three?: string;
}
type DuplicatedAndRenamed = Postfixer<Foo, ".$">;

这里的目标是创建类型为DuplicatedAndRenamed的对象可能具有的所有可能键组合的并集。联合的前两个元素看起来像这样:

{
"one.$"?: undefined;
one: string;
"two.$"?: undefined;
two: string;
"three.$"?: undefined;
three?: string | undefined;
} | {
"one.$"?: undefined;
one: string;
"two.$"?: undefined;
two: string;
three?: undefined;
"three.$": string | undefined;
} | ... 9 more ... | {
...;
}

可以看到,对于每种可能的情况,联合的每个元素都精确地指定了哪些属性必须定义,哪些必须是undefined

这个实现使用了两个方便的实用程序类型。XOR从这个答案将允许我们强制使用带后缀的键或不带后缀的键,但不能同时使用.

我们还需要这个答案中的UnionToIntersection将联合转换为交叉点(正如名称所暗示的那样)。

对于Postfixer的实现,我们映射到T的键。对于每个键,我们创建一个包含键的XOR'd类型和后置键的元组。如果T的键是可选的,则在联合中添加另一个元素,其中两个键都是undefined

| (undefined extends T[P]
? { [Key in Postfix<K, string & P> | P]: undefined }
: never)

可以看到,计算的XOR类型被包装在一个元组中。这将防止UnionToIntersectionXOR'd类型相交。在所有映射键的并集相交之后,我们只需要使用[any]来访问生成的索引类型。

让我们看看它是否通过了所有的测试:

// works
const shouldWork1: DuplicatedAndRenamed = {
"one.$": 'foo',
"three.$": 'bar',
"two.$": 'baz',
}
// works: mix of postfix and non-postfix
const shouldWork2: DuplicatedAndRenamed = {
"one.$": 'foo',
three: 'bar',
"two.$": 'baz',
}
// works: three is not here because optional
const shouldWork3: DuplicatedAndRenamed = {
"one.$": 'foo',
"two.$": 'baz',
}
// Error: three and three.$ are not allowed together
const shouldNotWork: DuplicatedAndRenamed = {
"one.$": 'foo',
three: 'bar',
"three.$": 'bar',
"two.$": 'baz',
}

游乐场

最新更新