英文:
Type like `Record<string, keyof T>` that guarantees each key in `keyof T` is used?
问题
class Person{
public Name: string = '';
public Age: number = 0;
}
class Some {
public StringToKey: Record<string, keyof Person> = {
"N": "Name",
"A": "Age"
}
public KeyToVal: {[Key in keyof Person]: (value:string)=>Person[Key]} = {
Name: (value: string) => {return value;},
Age: (value: string) => {return Number(value);}
}
}
英文:
I make a simple example just to show a problem. There it is
class Person{
public Name: string = '';
public Age: number = 0;
}
class Some {
public StringToKey: Record<string, keyof Person> = {
"N": "Name",
"A": "Age"
}
public KeyToVal: {[Key in keyof Person]: (value:string)=>Person[Key]} = {
Name: (value: string) => {return value;},
Age: (value: string) => {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<string, keyof T>
这样的特定类型,因为没有直接实现 ExhaustiveRecord
的方法。
相反,我们能做的最好的事情就是编写一个泛型类型 ExhaustivePersonKeyRecord<T>
,它充当了 T
的约束,以便只有当 T
对 Person
的每个键都具有属性值时,T extends ExhaustivePersonKeyRecord<T>
才为真。
一旦我们有了这个,我们会编写一个使用这种类型的泛型助手函数,就像这样:
function exhaustivePersonKeyRecord<T extends Record<keyof T, keyof Person>>(
t: ExhaustivePersonKeyRecord<T>
) { return t }
这将使得 exhaustivePersonKeyRecord({a: "Name", b: "Age"})
成功,但 exhaustivePersonKeyRecord({a: "Name"})
失败。
我们所要做的就是定义一个合适的 ExhaustivePersonKeyRecord<T>
类型。以下是一种可能的方法:
declare const __some_property__: unique symbol;
type ExhaustivePersonKeyRecord<T> = T & (keyof Person extends T[keyof T] ? unknown :
{ [__some_property__]: Exclude<keyof Person, T[keyof T]> }
)
这是 T
与比较 keyof Person
与 T[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 & unknown
就是 T
。而且 T extends T
,因此它将成功。
如果该检查为假,即您漏掉了一些 Person
的键,那么条件类型将计算为 { [__some_property__]: Exclude<keyof Person, T[keyof T]> }
。这是一个对象类型,具有类型 __some_property__
的键(我声明了一个虚假的符号类型,只是为了有一些“键”来表示我们漏掉了)和类型 Exclude<keyof Person, T[keyof T]>
的值,使用Exclude 实用程序类型确定了缺少的键。如果 T
是,比如说,{a: "Name"}
,那么 Exclude<keyof Person, T[keyof T]>
将是 "Age"
,整个类型将计算为 {a: "Name"} & {[__some_property__]: "Age"}
,因此 T extends ExhaustivePersonKeyRecord<T>
将为假,它将失败。
让我们来测试一下:
class Some {
public StringToKey = exhaustivePersonKeyRecord({
"N": "Name",
"A": "Age",
}); // okay
}
这在我们扩展 Person
之前是有效的:
class Person {
public Name: string = '';
public Age: number = 0;
public Height: number = 0; // <-- do this
}
然后我们会得到所期望的错误:
class Some {
public StringToKey = exhaustivePersonKeyRecord({
"N": "Name",
"A": "Age"
}); // error
}
// Property '[__some_property__]' is missing in type '{ N: "Name"; A: "Age"; }'
// but required in type '{ [__some_property__]: "Height"; }'.
希望这能为我们提供足够的信息来解决问题:
class Some {
public StringToKey = exhaustivePersonKeyRecord({
"N": "Name",
"A": "Age",
"H": "Height"
}); // 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<string, keyof T>
exists, since there's no way to implement ExhaustiveRecord
directly.
Instead the best we can do is write a generic type ExhaustivePersonKeyRecord<T>
that acts as a constraint on T
so that T extends ExhaustivePersonKeyRecord<T>
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<T extends Record<keyof T, keyof Person>>(
t: ExhaustivePersonKeyRecord<T>
) { return t }
That will make it so that exhaustivePersonKeyRecord({a: "Name", b: "Age"})
will succeed but exhaustivePersonKeyRecord({a: "Name"})
will fail.
All we have to do is define an appropriate ExhaustivePersonKeyRecord<T>
type. Here's one possible way to do it:
declare const __some_property__: unique symbol;
type ExhaustivePersonKeyRecord<T> = T & (keyof Person extends T[keyof T] ? unknown :
{ [__some_property__]: Exclude<keyof Person, T[keyof T]> }
)
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 & 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<keyof Person, T[keyof T]> }
. 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<keyof Person, T[keyof T]>
, using the Exclude
utility type to determine the missing keys. If T
is, say, {a: "Name"}
, then Exclude<keyof Person, T[keyof T]>
will be "Age"
, and the whole type will evaluate to {a: "Name"} & {[__some_property__]: "Age"}
, and so T extends ExhaustivePersonKeyRecord<T>
will be false, and it will fail.
Let's test it out:
class Some {
public StringToKey = exhaustivePersonKeyRecord({
"N": "Name",
"A": "Age",
}); // okay
}
That works until we augment Person
:
class Person {
public Name: string = '';
public Age: number = 0;
public Height: number = 0; // <-- do this
}
Then we get the desired error:
class Some {
public StringToKey = exhaustivePersonKeyRecord({
"N": "Name",
"A": "Age"
}); // error
}
// Property '[__some_property__]' is missing in type '{ N: "Name"; A: "Age"; }'
// but required in type '{ [__some_property__]: "Height"; }'.
which hopefully gives us enough information to fix it:
class Some {
public StringToKey = exhaustivePersonKeyRecord({
"N": "Name",
"A": "Age",
"H": "Height"
}); // okay
}
Looks good!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论