我有一个带有可选字段的类型。通过类型实用程序,我使其中一个成为必需的,并且我可以通过这个现在需要的键索引到更深的对象。
但是当我在泛型函数中执行完全相同的操作时,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
具有可能undefined
的x
属性。 因此,我们不能假设-?
修饰符必然会产生具有所有已定义属性的类型。
这是按预期工作的,而不是错误。
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
属性,其值为string
或undefined
。 有些人可能会惊讶地发现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
,但值req
是string | undefined
类型,值mis
是unknown
类型。 因此,如果没有像这样一些额外的处理,你就无法对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 !!
操场链接到代码