`Record` 可以确保在 `keyof T` 中的每个键都被使用。

huangapple go评论54阅读模式
英文:

Type like `Record<string, keyof T>` that guarantees each key in `keyof T` is used?

问题

class Person{
    public Name: string = &#39;&#39;;
    public Age: number = 0;
}

class Some {
    public StringToKey: Record&lt;string, keyof Person&gt; = {
        &quot;N&quot;: &quot;Name&quot;,
        &quot;A&quot;: &quot;Age&quot;
    }
    public KeyToVal: {[Key in keyof Person]: (value:string)=&gt;Person[Key]} = {
        Name: (value: string) =&gt; {return value;},
        Age: (value: string) =&gt; {return Number(value);}
    }
}
英文:

I make a simple example just to show a problem. There it is

class Person{
    public Name: string = &#39;&#39;;
    public Age: number = 0;
}

class Some {
    public StringToKey: Record&lt;string, keyof Person&gt; = {
        &quot;N&quot;: &quot;Name&quot;,
        &quot;A&quot;: &quot;Age&quot;
    }
    public KeyToVal: {[Key in keyof Person]: (value:string)=&gt;Person[Key]} = {
        Name: (value: string) =&gt; {return value;},
        Age: (value: string) =&gt; {return Number(value);}
    }
}

All looks good and compile. If i add new property to Person, i want to get 2 errors: for StringToKey and KeyToVal, but i only have error in KeyToVal situation.

So, for StringToKey declaration i can't be sure that i will get error if something will be changed in Person class, which is bad for me.

Is there any magic in type declaration i could do to get error in StringToKey declaration when Person class has been changed?

答案1

得分: 0

以下是您要翻译的内容:

TypeScript 真的没有一种表示“穷尽”的类型的方式,以确保某个联合的每个成员都已知存在作为属性值。因此,不存在像 type ExhaustivePersonKeyRecord = ExhaustiveRecord&lt;string, keyof T&gt; 这样的特定类型,因为没有直接实现 ExhaustiveRecord 的方法。


相反,我们能做的最好的事情就是编写一个泛型类型 ExhaustivePersonKeyRecord&lt;T&gt;,它充当了 T约束,以便只有当 TPerson 的每个键都具有属性值时,T extends ExhaustivePersonKeyRecord&lt;T&gt; 才为真。

一旦我们有了这个,我们会编写一个使用这种类型的泛型助手函数,就像这样:

function exhaustivePersonKeyRecord&lt;T extends Record&lt;keyof T, keyof Person&gt;&gt;(
  t: ExhaustivePersonKeyRecord&lt;T&gt;
) { return t }

这将使得 exhaustivePersonKeyRecord({a: &quot;Name&quot;, b: &quot;Age&quot;}) 成功,但 exhaustivePersonKeyRecord({a: &quot;Name&quot;}) 失败。


我们所要做的就是定义一个合适的 ExhaustivePersonKeyRecord&lt;T&gt; 类型。以下是一种可能的方法:

declare const __some_property__: unique symbol;
type ExhaustivePersonKeyRecord&lt;T&gt; = T &amp; (keyof Person extends T[keyof T] ? unknown :
  { [__some_property__]: Exclude&lt;keyof Person, T[keyof T]&gt; }
)

这是 T 与比较 keyof PersonT[keyof T] 的条件类型的交集。类型 T[keyof T] 是包含在 T 中的所有属性类型的联合(如 https://stackoverflow.com/q/49285864/2887218 中所讨论的)。如果 keyof Person extends T[keyof T],这意味着 T[keyof T]keyof Person 更宽,因此包含了每个成员。这是您想要的。

如果该检查为真,并且您在 T 的属性中使用了所有 Person 的键,那么条件类型将计算为未知类型T &amp; unknown 就是 T。而且 T extends T,因此它将成功。

如果该检查为假,即您漏掉了一些 Person 的键,那么条件类型将计算为 { [__some_property__]: Exclude&lt;keyof Person, T[keyof T]&gt; }。这是一个对象类型,具有类型 __some_property__ 的键(我声明了一个虚假的符号类型,只是为了有一些“键”来表示我们漏掉了)和类型 Exclude&lt;keyof Person, T[keyof T]&gt; 的值,使用Exclude 实用程序类型确定了缺少的键。如果 T 是,比如说,{a: &quot;Name&quot;},那么 Exclude&lt;keyof Person, T[keyof T]&gt; 将是 "Age",整个类型将计算为 {a: &quot;Name&quot;} &amp; {[__some_property__]: &quot;Age&quot;},因此 T extends ExhaustivePersonKeyRecord&lt;T&gt; 将为假,它将失败。


让我们来测试一下:

class Some {
  public StringToKey = exhaustivePersonKeyRecord({
    &quot;N&quot;: &quot;Name&quot;,
    &quot;A&quot;: &quot;Age&quot;,
  }); // okay
}

这在我们扩展 Person 之前是有效的:

class Person {
  public Name: string = &#39;&#39;;
  public Age: number = 0;
  public Height: number = 0; // &lt;-- do this
}

然后我们会得到所期望的错误:

class Some {
  public StringToKey = exhaustivePersonKeyRecord({
    &quot;N&quot;: &quot;Name&quot;,
    &quot;A&quot;: &quot;Age&quot;
  }); // error
}
//  Property &#39;[__some_property__]&#39; is missing in type &#39;{ N: &quot;Name&quot;; A: &quot;Age&quot;; }&#39;
// but required in type &#39;{ [__some_property__]: &quot;Height&quot;; }&#39;.

希望这能为我们提供足够的信息来解决问题:

class Some {
  public StringToKey = exhaustivePersonKeyRecord({
    &quot;N&quot;: &quot;Name&quot;,
    &quot;A&quot;: &quot;Age&quot;,
    &quot;H&quot;: &quot;Height&quot;
  }); // okay
}

看起来不错!

英文:

TypeScript really doesn't have a way to represent an "exhaustive" type that ensures that each member of some union is known to be present as a property value. So no specific type like type ExhaustivePersonKeyRecord = ExhaustiveRecord&lt;string, keyof T&gt; exists, since there's no way to implement ExhaustiveRecord directly.


Instead the best we can do is write a generic type ExhaustivePersonKeyRecord&lt;T&gt; that acts as a constraint on T so that T extends ExhaustivePersonKeyRecord&lt;T&gt; if and only if T has a property value for every key of Person.

Once we have that, we'd write a generic helper identity function that uses such a type, like this:

function exhaustivePersonKeyRecord&lt;T extends Record&lt;keyof T, keyof Person&gt;&gt;(
  t: ExhaustivePersonKeyRecord&lt;T&gt;
) { return t }

That will make it so that exhaustivePersonKeyRecord({a: &quot;Name&quot;, b: &quot;Age&quot;}) will succeed but exhaustivePersonKeyRecord({a: &quot;Name&quot;}) will fail.


All we have to do is define an appropriate ExhaustivePersonKeyRecord&lt;T&gt; type. Here's one possible way to do it:

declare const __some_property__: unique symbol;
type ExhaustivePersonKeyRecord&lt;T&gt; = T &amp; (keyof Person extends T[keyof T] ? unknown :
  { [__some_property__]: Exclude&lt;keyof Person, T[keyof T]&gt; }
)

That's T intersected with a conditional type that compares keyof Person to T[keyof T]. The type T[keyof T] is the union of all property types included in T (as discussed in https://stackoverflow.com/q/49285864/2887218 ). If keyof Person extends T[keyof T] it means that T[keyof T] is wider than keyof Person and thus contains every member. That's what you want.

If that check is true and you used all the keys of Person in the properties of T, then the conditional type evaluates to the unknown type and T &amp; unknown is just T. And T extends T, so it will succeed.

If that check is false, and you missed some key or keys of Person, then the conditional type evaluates to { [__some_property__]: Exclude&lt;keyof Person, T[keyof T]&gt; }. This is an object type with the key of type __some_property__ (a fake symbol type I declared just to have some "key" to say we missed) and a value of type Exclude&lt;keyof Person, T[keyof T]&gt;, using the Exclude utility type to determine the missing keys. If T is, say, {a: &quot;Name&quot;}, then Exclude&lt;keyof Person, T[keyof T]&gt; will be &quot;Age&quot;, and the whole type will evaluate to {a: &quot;Name&quot;} &amp; {[__some_property__]: &quot;Age&quot;}, and so T extends ExhaustivePersonKeyRecord&lt;T&gt; will be false, and it will fail.


Let's test it out:

class Some {
  public StringToKey = exhaustivePersonKeyRecord({
    &quot;N&quot;: &quot;Name&quot;,
    &quot;A&quot;: &quot;Age&quot;,
  }); // okay
}

That works until we augment Person:

class Person {
  public Name: string = &#39;&#39;;
  public Age: number = 0;
  public Height: number = 0; // &lt;-- do this
}

Then we get the desired error:

class Some {
  public StringToKey = exhaustivePersonKeyRecord({
    &quot;N&quot;: &quot;Name&quot;,
    &quot;A&quot;: &quot;Age&quot;
  }); // error
}
//  Property &#39;[__some_property__]&#39; is missing in type &#39;{ N: &quot;Name&quot;; A: &quot;Age&quot;; }&#39;
// but required in type &#39;{ [__some_property__]: &quot;Height&quot;; }&#39;.

which hopefully gives us enough information to fix it:

class Some {
  public StringToKey = exhaustivePersonKeyRecord({
    &quot;N&quot;: &quot;Name&quot;,
    &quot;A&quot;: &quot;Age&quot;,
    &quot;H&quot;: &quot;Height&quot;
  }); // okay
}

Looks good!

Playground link to code

huangapple
  • 本文由 发表于 2023年6月15日 02:37:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/76476640.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定