Typescript:将数据从对象类型转换为联合类型的类型安全方法

1dkrff03  于 2023-03-24  发布在  TypeScript
关注(0)|答案(2)|浏览(176)

我正在寻找一种类型安全的方法来执行以下类型的数据转换:

// original data type
const nameToDaysAsWWEChamp = {
  "Stone Cold Steve Austin": 529,
  "The Rock": 378,
  "Hulk Hogan": 2188,
  "Ric Flair": 118,
} as const;

// target data type
type TargetType =
  | { name: "Stone Cold Steve Austin"; daysAsWWEChamp: 529 }
  | { name: "The Rock"; daysAsWWEChamp: 378 }
  | { name: "Hulk Hogan"; daysAsWWEChamp: 2188 }
  | { name: "Ric Flair"; daysAsWWEChamp: 118 };

// attempted transformation
function f(name: keyof typeof nameToDaysAsWWEChamp): TargetType {
  return { name, daysAsWWEChamp: nameToDaysAsWWEChamp[name] }; // error!
}

函数f想要返回以下类型的对象:

{
    name: "Stone Cold Steve Austin" | "The Rock" | "Hulk Hogan" | "Ric Flair";
    daysAsWWEChamp: 529 | 378 | 2188 | 118;
}

我如何才能说服typescript“尊重Map”并将f的返回类型缩小到TargetType

q8l4jmvw

q8l4jmvw1#

TypeScript无法抽象f的主体,以理解将name缩小到联合类型keyof typeof nameToDaysAsWWEChamp的特定成员的每一种可能性,在输出为{name, daysAsWWEChamp: nameToDaysAsWWEChamp[name]}时,都会导致有效的TargetType。它根本不会尝试这样分析;相反,它只是将name的类型作为联合,因此daysAsWWEChamp也是一个联合,并导致你提到的太宽的类型。它不明白name的联合类型与daysAsWWEChamp的联合类型是相关的。
这是microsoft/TypeScript#30581的主题;TypeScript并不真正支持相关的联合类型。在microsoft/TypeScript#47109中描述了这种情况下的建议方法...它涉及使用generics而不是联合。你必须重构你的类型,以便将操作视为通用的indexed accesses转换为mapped types。你可以阅读问题以了解对此的详细描述。对于你的示例,它看起来像这样:

// this just makes it easier to refer to the type
type NameToDaysAsWWEChamp = typeof nameToDaysAsWWEChamp;

type TargetType<K extends keyof NameToDaysAsWWEChamp = keyof NameToDaysAsWWEChamp> =
    { [P in K]: { name: P, daysAsWWEChamp: NameToDaysAsWWEChamp[P] } }[K];

这里TargetType现在是一个泛型 * 分布式对象类型 *。如果你指定泛型类型参数作为NameToDaysAsWWEChamp的某个特定键,你会得到原始TargetType联合的对应成员。如果你指定一个 union,你也会得到对应的联合。如果你根本不指定它,你会得到完整keyof NameToDaysAsWWEChamp的默认值,所以它和你以前的TargetType是一样的,这意味着在某种意义上,TargetType和以前是一样的:

type Same = TargetType;
/* type Same = {
    name: "Stone Cold Steve Austin";
    daysAsWWEChamp: 529;
} | {
    name: "The Rock";
    daysAsWWEChamp: 378;
} | {
    name: "Hulk Hogan";
    daysAsWWEChamp: 2188;
} | {
    name: "Ric Flair";
    daysAsWWEChamp: 118;
} */

现在,我们可以将f写成一个 generic 函数,它接受K类型的name参数,并返回TargetType<K>类型的输出:

function f<K extends keyof NameToDaysAsWWEChamp>(name: K): TargetType<K> {
    return { name, daysAsWWEChamp: nameToDaysAsWWEChamp[name] }; // okay
}

这是可行的,因为编译器将输出视为{ name: K, daysAsWWEChamp: NameToSaysAsWWEChamp[K] },这与定义TargetType<K>相同(至少对于K是单个文字类型;对于联合体来说,这有点复杂,编译器做了一些技术上不安全的事情,参见microsoft/TypeScript#48730,但这是故意的,这里的行为是你想要的)。
作为额外的奖励,f()函数的输出将比TargetType更具体、更窄,如果需要,可以使用这些更窄的类型:

const hh = f("Hulk Hogan");
// const hh: { name: "Hulk Hogan"; daysAsWWEChamp: 2188; }

const mineral = f(Math.random() < 0.5 ? "The Rock" : "Stone Cold Steve Austin");
// const mineral: TargetType<"Stone Cold Steve Austin" | "The Rock">
if (mineral.daysAsWWEChamp === 378) {
    mineral.name;
    //      ^? (property) name: "The Rock"
} else {
    mineral.name;
    //      ^? (property) name: "Stone Cold Steve Austin"
}

编译器知道hh是一个特定的TargetType成员,而mineral是一对特定成员中的一个。
Playground代码链接

x7yiwoj4

x7yiwoj42#

这个有用吗?
//尝试转换

function f(name: keyof typeof nameToDaysAsWWEChamp):TargetType {
    return { name, daysAsWWEChamp: nameToDaysAsWWEChamp[name] } as TargetType;
}

相关问题