【问题标题】:Detecting when Partial<T> extends T检测 Partial<T> 何时扩展 T
【发布时间】:2021-08-16 23:10:57
【问题描述】:

我有一个用例构建到目标“状态”:

type State = { foo: number, bar: number, baz?: string };

当我开始时,我很可能还没有达到完整的状态,但我很高兴能适应Partial&lt;State&gt; 的形状。我想做什么,当我达到传说中的State 状态时如何推断。

为了帮助我实现这个目标,我首先创建了一个名为 Resolve 的实用程序类型:

type Resolve<T extends Partial<State>> = 
    T extends infer U 
        ? U extends State ? State : Partial<State> 
        : never;

当给定这样的推断类型时,此实用程序可以工作:

const implicit1 = { foo: 5 };
const implicit2 = { foo: 5, bar: 10 };
// YAY: resolves to Partial<State>
const implicitResolve1: Resolve<typeof implicit1> = implicit1;
// YAY: resolves to State
const implicitResolve2: Resolve<typeof implicit2> = implicit2;

然而,一旦一个类型被表达为Partial&lt;State&gt;,它就拒绝推断它可能是State类型:

const explicit1: Partial<State> = { foo: 5 };
const explicit2: Partial<State> = { foo: 5, bar: 10 };
// YAY: correctly resolves to Partial<State>
const explicitResolve1: Resolve<typeof explicit1> = explicit1;
// SAD FACE: also resolves to Partial<State> even though the intent 
// was for it to be recognized that it is a valid State!
const explicitResolve2: Resolve<typeof explicit2> = explicit2;

在我更广泛的解决方案中,我已经有一个类型后卫在等待,我认为这会给我我需要的超能力:

type TypeGuard<T> = (thing: unknown) => thing is T;
const tg: TypeGuard<State> = (thing: unknown): thing is State => {
    return typeof thing === "object" 
      && typeof (thing as any)?.foo === "number" 
      && typeof (thing as any)?.bar === "number";
};
function SuperResolve(thing: unknown, tg: TypeGuard<State>) {
    return tg(thing) ? thing as State : thing as Partial<State>
}
// SHOCKED: this resolves to Partial<State> too!
const fnResolve = SuperResolve(explicit2, tg);

我现在的能力已经到了尽头......当然有一些方法可以检测Partial&lt;T&gt; 何时达到&lt;T&gt;

Code Playground

【问题讨论】:

  • 我脑子里唯一能想到的可能是Required<T>
  • 我不确定一旦你用类型明确地标记了它,你是否能够获得该值的“实际”类型。例如,您可以使用const explicit3: Partial&lt;State&gt; = new Date as any
  • @ErikPhilips 如果我在 Resolve 实用程序中将 Partial 强制转换为 Required>,则会失败。您打算如何使用它?
  • @y2bd 并没有真正理解你的意思……any 的分配让这个例子变得毫无用处。目的是使用部分结构,并且随着时间的推移构建它,您会测试它是否达到“完成”状态。
  • @ken 我相信,一旦您告诉 TypeScript 一个值的显式类型(在您的情况下是通过键入变量),TS 就会“丢弃”它自己为该值推断的类型。这意味着您无法再获取值的推断类型并将其视为Partial&lt;State&gt; 之外的任何内容,类似于将某些内容转换为any 时,您将无法再看到值的原始类型是什么(在我的情况下,日期)。

标签: typescript typescript-typings typescript-generics


【解决方案1】:

如果您将变量注释为单个对象类型,例如 Partial&lt;State&gt;(等效于 { foo?: number, bar?: number, baz?: string }),TypeScript 编译器将不会 narrow 在为其分配更具体类型的值时变量的表观类型.

这种基于控制流的缩小只发生在union 类型的变量/属性上,而Partial&lt;State&gt; 本身不是联合类型。

所以,只要你写完

const x: Partial<State> = { foo: 5, bar: 10 };

您已丢弃编译器可能拥有的有关您分配给x 的特定值的任何信息。变量x 的类型为Partial&lt;State&gt;,无论您分配给它什么。您可以使用用户定义的类型保护和控制流分析来有条件地缩小范围,但这与分配给x 的实际值无关:

if (tg(x)) {
    x.bar.toFixed(2); // okay
} 

如果您希望编译器记住 x 可分配给 State,则不应抢先将其扩展为 Partial&lt;State&gt;。只需让编译器推断x的类型即可:

const x = { foo: 5, bar: 10 }; // okay

由于 TypeScript 的 structural type system,任何需要 StatePartial&lt;State&gt; 的东西都会很乐意接受 x

function takeState(state: State) { }
takeState(x); // okay

如果您想确保 x 在分配时确实是 Partial&lt;State&gt; 并且不仅在以后捕获错误,您可以使用类似的帮助函数

const asPartialState = <T extends Partial<State>>(t: T) => t;

并验证它是否根据Partial&lt;State&gt; 检查其输入的类型,而不会将其扩大Partial&lt;State&gt;

const y = asPartialState({ foo: 5, bar: 10 }); // okay
takeState(y); // okay

const z = asPartialState({ foo: 5, bar: "oops", baz: "" }); // error!
// ------------------------------> ~~~
// Type 'string' is not assignable to type 'number | undefined'.

这些都不允许您将Partial&lt;State&gt; 的任何“构建”到State,因为变量的类型不会在分配时发生变化(我在您的问题中也没有看到任何这样的构建,所以我不确定这是否在范围内。假设它在范围内。)

所以,这行不通:

const x = { foo: 5 }
x.bar = 10; // error!
takeState(x); // error!

const y: Partial<State> = { foo: 5 }
y.bar = 10; // okay
takeState(y); // error!

如果您打算通过手动分配每个属性来进行此构建,则可以使用 assertion function 之类的东西来缩小控制流:

function setProp<T extends object, K extends PropertyKey, V>(
    obj: T, key: K, val: V
): asserts obj is T & Record<K, V> { (obj as any)[key] = val }

const x = { foo: 5 }
setProp(x, "bar", 10); // okay now
takeState(x); // okay

如果您希望做一些更复杂或抽象的“构建”,在其中循环属性或将内容传递给本身不是断言函数的其他函数,即使这样也行不通:

const x = {};
(["foo", "bar"] as const).forEach(k => setProp(x, k, 5));
takeState(x); // error!

在这种情况下,您应该放弃让编译器尝试遵循相对复杂的控制流,该控制流证明您的 Partial&lt;State&gt; 已成长为 State,并且只是 assert 已经完成(使用可能会丢失类型安全性):

takeState(x as State); // no error, but maybe you're wrong

或者用你的类型保护函数做一个不必要的运行时检查:

if (!tg(x)) throw new Error("My life is a lie"); 
takeState(x); // okay

Playground link to code

【讨论】:

  • 我确实需要在我更广泛的示例中 build 以便您后面的示例真的一针见血。再次感谢您成为 SO 上最聪明的 TS'er。
  • 我已经发布了我自己的答案,你能看看你是否同意?在我听到您同意之前,我会将所选答案保留为您的答案。
  • @ken 我不确定用explicit 重新注释implicit 的值是对类似asPartialState() 函数的改进。显然你负责选择哪个答案,所以我没有太多的说法。通常,我会尝试确保我的答案具有文档链接,并有望成为对未来读者和原始提问者有所帮助的答案(这就是我详细介绍“构建”部分的原因)。
  • 是的,你的权利,当我尝试使用它时,它并没有像我希望的那样工作。我将删除我的解决方案,因为我认为它增加了有限的实用性。
【解决方案2】:

StatePartial&lt;State&gt; 的完整子集,这就是State | Partial&lt;State&gt; == Partial&lt;State&gt; 的原因。

Partial&lt;State&gt; 强制转换为State 的唯一方法是显式设置Required&lt;State&gt; 或使用type-fest 并设置SetRequired&lt;Partial&lt;State&gt;, "foo" | "bar"&gt;(但这意味着您可以从某处提取密钥)。

以下代码:

const explicit2: Partial<State> = { foo: 5, bar: 10 };
const explicitResolve2: Resolve<typeof explicit2> = explicit2;

这是一个错误,因为您在设置: Partial&lt;State&gt; 时剥离了所有有关义务的信息,= 之后存在的内容并不重要。

我建议你获得两个类型保护来断言你需要的类型。一个可以判断参数是否为Partial&lt;State&gt;,另一个判断参数是否为State,并在不同的if 语句中运行它们。

【讨论】:

  • 有没有办法将 Partial “减少”回 unknown 然后使用类型保护?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-25
  • 1970-01-01
  • 1970-01-01
  • 2011-02-02
  • 2016-05-08
  • 1970-01-01
相关资源
最近更新 更多