嵌套条件类型的评估 - 生成的类型未按预期运行



我有一个类型,它表示来自 API 调用的有效负载。如果在有效负载上定义了外键,则已知有效负载还包含与相关记录对应的元数据:

type Payload = {
id: number
districtAdmin?: string | null
// added after request for more info
dedicatedSupport?: string | null
edges: {
// if typeof districtAdmin === "string"
districtAdmin: {
name: string
}
// else
districtAdmin: never
// same with dedicatedSupport
}
}

这个游乐场是我实现这种类型的尝试(现在只尝试处理一种关系):

type UserDataPayload = {
id: string
username: string
}
type OrganizationPayload<
TR extends string | null = string | null,
> = {
id: number
districtAdmin?: TR
edges: {
districtAdmin?: TR extends string ? UserDataPayload : never
}
}

const daUser: UserDataPayload = {
id: "da",
username: "da.bob"
}
const daOrg: OrganizationPayload = {
id: 1,
districtAdmin: "da",
edges: {
districtAdmin: daUser
}
}
// (property) districtAdmin?: UserDataPayload | undefined
// Object is possibly 'undefined'.(2532)
console.log(daOrg.edges.districtAdmin.username)

你在这里得到的条件类型,

type OrganizationPayload<TR extends string | null = string | null> = {
id: number
districtAdmin?: TR
edges: {
districtAdmin?: TR extends string ? UserDataPayload : never
}
}

是泛型的,因此依赖于类型参数TR以指定districtAdmin属性是同时存在于顶级对象和edges子属性中(通过将string指定为TR),还是在两个位置都不存在(通过将null指定为TR)。


嗯,无论如何,这似乎是意图。 无论如何,该属性都是可选的,即使您将string指定为TR

type OPS = OrganizationPayload<string>;
/* type OPS = {
id: number;
districtAdmin?: string | undefined;
edges: {
districtAdmin?: UserDataPayload | undefined;
};
} */

所以也许类型实际上应该是

type OrganizationPayload<TR extends string | null = string | null> = (
TR extends string ? { districtAdmin: string } : { districtAdmin?: null }
) & {
id: number;
edges: (
TR extends string ? { districtAdmin: UserDataPayload } :
{ districtAdmin?: never }
)
};

现在导致

type OPS = OrganizationPayload<string>;
/* type OPS = {
districtAdmin: string;
} & {
id: number;
edges: {
districtAdmin: UserDataPayload;
};
} */
type OPN = OrganizationPayload<null>;
/* type OPN = {
districtAdmin?: null | undefined;
} & {
id: number;
edges: {
districtAdmin?: undefined;
};
} */

我认为这相当于你想表达的。


但是你把一个变量注释为只有类型OrganizationalPayload,没有类型参数:

const daOrg: OrganizationPayload = { ... }

也许您的意图是编译器应该查看初始化值并推断出最合适的参数TR。 事实并非如此。

您对OrganizationalPayload的定义为TR提供了联合类型string | null的默认值。 当您省略类型参数时,编译器仅使用该默认值:

// const daOrg: OrganizationPayload<string | null>

类型的计算结果为

type OP = OrganizationPayload;
/* type OP = ({
districtAdmin: string;
} | {
districtAdmin?: null | undefined;
}) & {
id: number;
edges: {
districtAdmin: UserDataPayload;
} | {
districtAdmin?: undefined;
};
} */

这是一种无论如何两个属性都是可选的类型,并且顶层存在districtAdmin并不意味着它将存在于edges中。 哎呀。

当您将该初始化值分配给变量时,编译器不会跟踪edges属性中的特定值。 指定edgesOrganizationPayload部分本身不是联合类型,因此在分配时不会缩小范围。 所以你得到错误:

console.log(daOrg.edges.districtAdmin.username) // error!
// -------> ~~~~~~~~~~~~~~~~~~~~~~~~~
// Object is possibly 'undefined'.

同样,没有直接的方法要求编译器从初始化值推断TR。 在microsoft/TypeScript#32794上有一个建议来支持这一点,如果这被实现,它可能看起来像

const daOrg: OrganizationPayload<infer> = { ... }

但就目前而言,它不是语言的一部分。 通常在这种情况下,人们编写通用的帮助程序函数来推断类型参数,例如

const asOrgPayload = <TR,>(op: OrganizationalPayload<TR>) => op;
const daOrg = asOrgPayload({ ... });

但是你对这种方法不感兴趣。 即使你是,也可能需要修改它才能使推理工作,因为从条件类型推断是很棘手的。


也许OrganizationalPayload<string | null>评估为错误的类型,因为它允许"交叉相关"项。 既然OrganizationalPayload<string>很好,OrganizationalPayload<null>很好,也许你想让OrganizationalPayload<string | null>评估到它们的并集。

如果是这样,您可以再次将OrganizationalPayload修改为分发条件类型:

type OrganizationPayload<TR extends string | null = string | null> =
TR extends unknown ? ((
TR extends string ? { districtAdmin: string } : { districtAdmin?: null }
) & {
id: number;
edges: (
TR extends string ? { districtAdmin: UserDataPayload } :
{ districtAdmin?: never }
)
}) : never;

现在OrganizationalPayload计算出正确的(尽管丑陋)类型:

type OP = OrganizationPayload;
/* type OP = (
{ districtAdmin: string; } & 
{ id: number;  edges: { districtAdmin: UserDataPayload; }; }
) | (
{ districtAdmin?: null | undefined; } & 
{ id: number; edges: { districtAdmin?: undefined; }; }
) */

现在正确地在分配时缩小范围,一切都表现得很精彩!

const daOrg: OrganizationPayload = {
id: 1,
districtAdmin: "da",
edges: { districtAdmin: daUser }
}
console.log(daOrg.edges.districtAdmin.username) // okay

当然,这个OrganizationPayload定义现在相当丑陋,里面有三种条件类型。 可以将它们重新排列为仅具有等效的单个条件类型:

type OrganizationPayload<TR extends string | null = string | null> =
{ id: number } & (
TR extends string ?
{ districtAdmin: string; edges: { districtAdmin: UserDataPayload } } :
{ districtAdmin?: null; edges: { districtAdmin?: never } }
);

甚至可能只是:

type OrganizationPayload<TR extends string | null = string | null> =
TR extends string ?
{ id: number; districtAdmin: string; edges: { districtAdmin: UserDataPayload } } :
{ id: number; districtAdmin?: null; edges: { districtAdmin?: never } };

如果您不指定TR,则变为:

type OP = OrganizationPayload;
/* type OP = 
{ id: number; districtAdmin: string; edges: { districtAdmin: UserDataPayload; }; } | 
{ id: number; districtAdmin?: null; edges: { districtAdmin?: never; };     
*/

普通联合类型。

这意味着条件类型虽然可能对类型开发人员有用,因此它们不必重复内容,但实际上对该类型的用户没有用......联合是执行您所期望的缩小的受支持方法。

但你对这种方法也不感兴趣。 工会对你来说是不行的,大概是因为如果你写这个(像这样),你的类型最终会得到两个工会成员的巨大权力。


所以在这一点上,我会说我被困住了。 条件类型本身不会为你提供你要查找的行为。 您需要使用通用帮助程序函数或联合进行推理和/或分配范围。 或者其他一些可能的方法(我在这里没有进入存在量化的泛型类型,因为它们不是语言的直接组成部分,即使它们是,你也不会得到你显然正在寻找的范围。

希望至少你明白为什么当前的方法不起作用。

操场链接到代码

最新更新