如何在 TypeScript 中强制属性之间的相互依赖。

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

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&lt;
  T extends string | undefined,
  PropertyName extends string | undefined,
&gt; = T extends string
  ? ({
      [K in T]: string;
    } &amp; { [K in PropertyName]: () =&gt; void })
  : {
      T?: undefined;
      PropertyName?: undefined;
    };

    //Alternatively:
    
    type InterConnectedProps2&lt;
  T extends string | undefined,
  PropertyName extends string | undefined,
&gt; = T extends string
  ? ({
      [K in T]: string;
      //The differnce lies here: &quot;as string&quot;
    } &amp; { [K in PropertyName as string]: () =&gt; void })
  : {
      T?: undefined;
      PropertyName?: undefined;
    };



type Props = {
  title: string;
  subtitle?: string;
}&amp; InterConnectedProps2&lt;&#39;button1&#39;, &#39;handlerBtn1&#39;&gt; &amp; InterConnectedProps2&lt;&#39;button2&#39;, &#39;handlerBtn2&#39;&gt;;

// Props should be either:
//  {title: string; subtitle?:string; button1: string; handlerBtn1 : () =&gt; 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&lt;T&gt; 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&lt;T extends object&gt; =
  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&lt;{ a: 0, b: 1, c: 2 }&gt;;
/* 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; } &amp;
  AllOrNone&lt;{ button1: string, handlerBtn1(): void }&gt; &amp;
  AllOrNone&lt;{ button2: string, handlerBtn2(): void }&gt;;

let p: Props;
p = { title: &quot;&quot; };
p = { title: &quot;&quot;, button1: &quot;&quot;, handlerBtn1() { } };
p = { title: &quot;&quot;, button2: &quot;&quot;, handlerBtn2() { } };
p = { title: &quot;&quot;, button1: &quot;&quot;, handlerBtn2() { } }; // error

Looks good!

Playground link to code

huangapple
  • 本文由 发表于 2023年4月10日 18:35:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/75976329.html
匿名

发表评论

匿名网友

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

确定