英文:
How to enforce interdependence among properties in Typescript
问题
I am trying to enforce interdependence among properties in a general TypeScript type. The context is a reusable React component that receives props. If the component receives a text for a button, for example, then it must also receive a handler for the button; but if the component doesn't receive the text, then the handler prop should also be undefined.
This is my attempt:
type InterConnectedProps<
T extends string | undefined,
PropertyName extends string | undefined,
> = T extends string
? ({
[K in T]: string;
} & { [K in PropertyName]: () => void })
: {
T?: undefined;
PropertyName?: undefined;
};
//Alternatively:
type InterConnectedProps2<
T extends string | undefined,
PropertyName extends string | undefined,
> = T extends string
? ({
[K in T]: string;
//The differnce lies here: "as string"
} & { [K in PropertyName as string]: () => void })
: {
T?: undefined;
PropertyName?: undefined;
};
type Props = {
title: string;
subtitle?: string;
} & InterConnectedProps2<'button1', 'handlerBtn1'> & InterConnectedProps2<'button2', 'handlerBtn2'>;
// Props should be either:
// {title: string; subtitle?:string; button1: string; handlerBtn1 : () => void}
// or {title: string; subtitle?:string; button1: undefined; handlerBtn1 : undefined}
// Anyway, there should be no handlerBtn1 if there is no button1
But this doesn't work, not only because I am not verifying that PropertyName is a string (how do I introduce multiple conditions?). Even if I write { [K in PropertyName as string]: () => void }, the resulting type seems not to include the second parameter (PropertyName):
type Button = InterConnectedProps<'button1', 'btn1Handler'> is just {button1: string}.
Where am I going wrong, or is there another alternative?
Thanks!
英文:
I am trying to enforce interdependence among properties in a general TypeScript type. The context is a reusable React component that receives props. If the component receives a text for a button, for example, then it must also receive a handler for the button; but if the component doesn't receive the text, then the handler prop should also be undefined.
This is my attempt:
type InterConnectedProps<
T extends string | undefined,
PropertyName extends string | undefined,
> = T extends string
? ({
[K in T]: string;
} & { [K in PropertyName]: () => void })
: {
T?: undefined;
PropertyName?: undefined;
};
//Alternatively:
type InterConnectedProps2<
T extends string | undefined,
PropertyName extends string | undefined,
> = T extends string
? ({
[K in T]: string;
//The differnce lies here: "as string"
} & { [K in PropertyName as string]: () => void })
: {
T?: undefined;
PropertyName?: undefined;
};
type Props = {
title: string;
subtitle?: string;
}& InterConnectedProps2<'button1', 'handlerBtn1'> & InterConnectedProps2<'button2', 'handlerBtn2'>;
// Props should be either:
// {title: string; subtitle?:string; button1: string; handlerBtn1 : () => void}
// or {title: string; subtitle?:string; button1: undefined; handlerBtn1 : undefined}
//Anyway, there should be no handlerBtn1 if there is no button1
But this doesn't work, not only because I am not verifying that PropertyName is a string (how do I introduce multiple conditions?). Even if write { [K in PropertyName as string]: () => void }, the resulting type seem not to include the second parameter (PropertyName):
type Button = InterConnectedProps<'button1', 'btn1Handler'> is just {button1: string}.
Where am I going wrong, or is there another alternative?
Thanks!
答案1
得分: 2
以下是您要翻译的部分:
看起来您想定义一个实用类型 AllOrNone<T>
,其中 T
是一个具有所有必需属性的对象类型,其结果类型将接受类型为 T
的对象或不包含来自 T
的任何属性的对象。
以下是定义它的一种可能方式:
type AllOrNone<T extends object> =
T | { [P in keyof T]?: never }
这是一个 联合类型,用于捕捉 "要么...要么" 的部分。一个联合成员是 T
,另一个是 { [P in keyof T]?: never }
,这是 TypeScript 中最接近 "不包含 T
的属性的对象" 的方式。
让我们更详细地看看 { [P in keyof T]?: never }
。这是一个 映射类型,其中 T
的每个属性键都变成了 可选的 并且值类型是 不可能的 never
类型。因此,如果 T
是 {a: 0, b: 1, c: 2}
,那么 {[P in keyof T]?: never}
是 {a?: never, b?: never, c?: never}
。 TypeScript 实际上没有一种方式可以禁止一个属性。但是,类型为 never
的可选属性接近:您无法提供类型为 never
的值,因此您唯一的 "选项" 就是要么完全省略该属性(或根据编译器设置将其设置为 undefined
)。
现在我们可以测试一下:
type Z = AllOrNone<{ a: 0, b: 1, c: 2 }>;
/* type Z = { a: 0, b: 1, c: 2 } |
{ a?: never, b?: never, c?: never } */
let z: Z;
z = {a: 0, b: 1, c: 2}; // okay
z = {a: 0, b: 1}; // error!
z = {a: 0}; // error!
z = {}; // okay
这是有道理的;Z
要么是 {a: 0, b: 1, c: 2}
,要么是不包含这些属性的对象。
现在我们可以通过两次使用 AllOrNone
来定义 Props
:
type Props = { title: string; subtitle?: string; } &
AllOrNone<{ button1: string, handlerBtn1(): void }> &
AllOrNone<{ button2: string, handlerBtn2(): void }>;
let p: Props;
p = { title: "" };
p = { title: "", button1: "", handlerBtn1() { } };
p = { title: "", button2: "", handlerBtn2() { } };
p = { title: "", button1: "", handlerBtn2() { } }; // error
看起来不错!
英文:
It looks like you want to define a utility type AllOrNone<T>
where T
is an object type with all required properties, whose resulting type will accept either an object of type T
, or an object with none of the properties from T
.
Here's one possible way to define it:
type AllOrNone<T extends object> =
T | { [P in keyof T]?: never }
This is a union type to capture the "either-or" part. One union member is T
, and the other is { [P in keyof T]?: never }
, which is as close as we can get to "an object with none of the properties of T
" in TypeScript.
Let's examine { [P in keyof T]?: never }
more. It's a mapped type where each property key from T
is made [optional](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers] and the value type is the impossible never
type. So if T
is {a: 0, b: 1, c: 2}
, then {[P in keyof T]?: never}
is {a?: never, b?: never, c?: never}
. TypeScript doesn't really have a way to prohibit a property. But an optional property of type never
is close: you can't supply a value of type never
, so then the only "option" you have is to leave the property out entirely (or to set it to undefined
depending on your compiler settings).
Let's test it out:
type Z = AllOrNone<{ a: 0, b: 1, c: 2 }>;
/* type Z = { a: 0, b: 1, c: 2 } |
{ a?: never, b?: never, c?: never } */
let z: Z;
z = {a: 0, b: 1, c: 2}; // okay
z = {a: 0, b: 1}; // error!
z = {a: 0}; // error!
z = {}; // okay
This makes sense; Z
is either {a: 0, b: 1, c: 2}
, or it's an object without any of those properties.
Now we can define Props
by using AllOrNone
twice:
type Props = { title: string; subtitle?: string; } &
AllOrNone<{ button1: string, handlerBtn1(): void }> &
AllOrNone<{ button2: string, handlerBtn2(): void }>;
let p: Props;
p = { title: "" };
p = { title: "", button1: "", handlerBtn1() { } };
p = { title: "", button2: "", handlerBtn2() { } };
p = { title: "", button1: "", handlerBtn2() { } }; // error
Looks good!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论