类型操作在泛型函数内的行为不同.试图找出原因



我有一个带有可选字段的类型。通过类型实用程序,我使其中一个成为必需的,并且我可以通过这个现在需要的键索引到更深的对象。

但是当我在泛型函数中执行完全相同的操作时,TypeScript 的行为就像该字段不是必需的,并且不允许我索引到它。我检查函数中的派生类型以检查其签名,一切看起来都符合预期。但是 TS 仍然不允许我索引到对象中。

注释良好的复制游乐场链接

如果我更改MakeKeyRequired类型实用程序以从结果值中显式过滤掉未定义,我可以解决此问题,如下所示:

type MakeKeyRequired<Obj extends object, K extends keyof Obj> = Omit<Obj, K> & {
[key in K]-?: Exclude<Obj[K], undefined>;
}

(Exclude<..., undefined>被添加到实用程序中) - 然后它也按预期在函数内部工作,但在我看来这应该是不必要的,正如 TS 实际上能够派生正确的类型这一事实所证明的那样毕竟(参见操场上的最后一行)。

你们 TS 专家能在这里帮我吗?

点 #1:Required<T>不会从非可选属性中删除undefined

即使-?映射修饰符将可选属性转换为排除undefined的必需属性,它也不会对根据需要启动的属性执行任何操作。 让我们使用Required<T>实用程序类型来演示:

type XNotUndefined = Required<{ x?: string | undefined }>
// type XNotUndefined = { x: string; } 
type XMaybeUndefined = Required<{ x: string | undefined }>
// type XMaybeUndefined =  { x: string | undefined; } 

类型XMaybeUndefined具有可能undefinedx属性。 因此,我们不能假设-?修饰符必然会产生具有所有已定义属性的类型。

这是按预期工作的,而不是错误。


Point #2:属性可能缺失或undefined的类型可分配给具有可选属性的类型:

此外,TypeScript 中可选属性的结构子类型规则也很有趣。 让我们看一个带有可选属性的类型:

interface OptionalX {
x?: string;
}
function acceptOptionalX(x: OptionalX) { }

现在让我们在不删除undefined的情况下删除x上的可选修饰符:

interface RequiredX {
x: string | undefined;
}
declare const r: RequiredX;

因此,类型RequiredX的值肯定具有x属性,其值为stringundefined。 有些人可能会惊讶地发现RequiredX extends OptionalX

acceptOptionalX(r); // okay, because RequiredX extends OptionalX;

让我们完全删除x属性:

interface MissingX { }
declare const m: MissingX;

因此,类型MissingX的值可能根本没有x属性。 有些人也可能惊讶地了解到MissingX extends OptionalX

acceptOptionalX(m); // okay, because MissingX extends OptionalX;

这意味着如果你有一个泛型函数,其类型参数T约束为OptionalX,如下所示:

function foo<T extends OptionalX>() { }

然后,可以使用x属性是必需但可能undefined的类型指定T

foo<RequiredX>(); // okay

也可以使用已知x属性存在的类型指定T,因此仍可能undefined

foo<MissingX>(); // okay

这是按预期工作的,而不是错误。


点 #1 和点 #2 组合在一起产生您所看到的行为

如果我们将它们组合在一起,我们会得到这个:

declare function bar<T extends OptionalX>(): Required<T>["x"];
const opt = bar<OptionalX>() // string
const req = bar<RequiredX>() // string | undefined
const mis = bar<MissingX>() // unknown

Required<T>["x"]类型,其中T extends OptionalX不能知道排除undefined,并且不能知道扩展string。 虽然opt的类型是string,但值reqstring | undefined类型,值misunknown类型。 因此,如果没有像这样一些额外的处理,你就无法对Required<T>["x"]做任何事情:

declare function baz<T extends OptionalX>(): (Required<T>["x"] & string);
const opt2 = baz<OptionalX>() // string
const req2 = baz<RequiredX>() // string 
const mis2 = baz<MissingX>() // string 

因此,您看到的行为是有意为之的,而不是错误。


对于代码中的特定示例,您可以看到如下问题:

genericFunction<{one: undefined}>() // returns undefined
genericFunction<{}>() // returns unknown

请注意,您不能编写ReturnType<typeof genericFunction>并获得完全准确的东西,因为如果没有对更高种类的类型(如 microsoft/TypeScript#1213 中的请求)或任意泛型值(如 microsoft/TypeScript#17574 中的请求),编译器将只使用其约束解析泛型类型参数。 因此,ReturnType<typeof genericFunction>仅反映将NestedObj指定为类型参数时发生的情况:

type CalculatedType = ReturnType<typeof genericFunction>; // {deep: number}
genericFunction<NestedObj>() // {deep: number}

但是绝对有可能在deep不是已知属性的地方得到一些东西。


你去吧,这是预期的行为。好吧,在我提交 microsoft/TypeScript#46879 之前,我并没有完全预料到这一点,但现在@RyanCavanaugh(Microsoft 年 TS 团队的开发负责人)也向我展示了所期望的光明。

这肯定有点奇怪,尤其是在缺少属性的情况下,因为不健全会蔓延。 一般来说,如果X extends Y,如果K extends keyof Y,那么X[K] extends Y[K]应该是真的。 但是对于缺少/可选属性,则不正确:{} extends {x?: string}"x" extends keyof {x?: string},但unknown不会扩展string | undefined。 哎呀。 我认为编译器将允许Exclude<T["x"], undefined>分配给string,即使它最终可能会被unknown。 不健全可能是有用的,但很难推理。

所以在你的泛型函数中,你可以这样做:

type Fixed = Exclude<SrcWithRequiredOne["one"], undefined>["deep"]; // okay

编译器对此很满意,但要小心,因为仍然会弹出奇怪的事情:

function genericFunction2<T extends NestedObj>() {
return null as unknown as Exclude<Required<T>["one"], undefined>["deep"];
}
genericFunction2<NestedObj>() // number, okay
genericFunction2<{ one: undefined }>() // never !!
genericFunction2<{}>() // unknown !!

操场链接到代码

最新更新