TypeScript Conditionally optional/conditionally readonly properties

q0qdq0h2  于 4个月前  发布在  TypeScript
关注(0)|答案(6)|浏览(36)

有条件可选属性(对象类型和接口中)

🔍 搜索词

  • 有条件可选
  • 有条件可选
  • 有条件只读
  • 有条件只读

这些问题相关,但并不完全相同:

✅ 可实现性检查清单

我的建议满足以下准则:

  • 这不会对现有的TypeScript/JavaScript代码造成破坏性的改变
  • 这不会改变现有JavaScript代码的运行时行为
  • 这可以在不根据表达式的类型发出不同的JS的情况下实现
  • 这不是一个运行时特性(例如库功能、带有JavaScript输出的非ECMAScript语法、新的JS语法糖等)
  • 这个特性将与 TypeScript's Design Goals 的其他部分一致。

⭐ 建议/📃 动机示例

一种新的语法和关联的类型检查使得可以将属性标记为“有条件可选”,例如:

interface Calculate<T> {
  items: T[];
  
  /**
* Converts an item into a string for comparison.
* If `T` is already a string type, this is optional.
*/  
  getKey(? if T extends string): (item: T) => string;
}

这里的意图是,如果 getKeystring 或者是 string 的子类型,那么 getKey 是可选的;否则,getKey 是强制性的。因此,一个实现(例如)可以提供一个默认实现 getKey = str => str
目前可以使用混合交集和有条件Map类型来编写实现此功能的类型。然而,你会失去某些重要的人体工程学属性:

  • 交集类型、Map类型和有条件类型的语法比接口/对象类型复杂得多,也不如它们熟悉
  • 有条件类型和复杂的Map很可能会丢失jsdoc注解,导致开发者在稍后尝试使用该类型时无法获得智能感知
  • 复杂的类型无法像接口和对象类型那样进行扩展

💻 用例

一般来说,属性的任何属性都可以以相同的方式进行有条件处理:

type Example = <T, Flag extends boolean, Active extends boolean> = {
  // conditionally optional:
  convertToString (? if T extends string): (item: T) => string;

  // optional; made conditionally required:
  initialValue? (-? if T extends boolean): T; 

  // conditionally readonly:
  (readonly if Flag extends "permanent") flagValue: boolean;

  // conditionally defined:
  (parentId if Active extends true): string;
}

在大多数情况下,存在一个基本(但不完整的)解决方案:在所有地方都使用较为宽松的形式。例如,如果它主要被使用并且你不想忘记传递一个属性,只需将其设置为必需;如果它主要被访问并且你不想忘记属性存在,只需将其设置为可选等。
允许这些值以有条件的方式设置将只是更容易表达某些更复杂的特定领域约束,同时保持类型主要自包含且易于阅读。

kzmpq1sx

kzmpq1sx1#

这似乎至少在组合爆炸性方面(也许无法判定?)当从一个具体类型进行推断到Calculate<T>时,因为你可以写一些类似的东西

interface Calculate<T> {
  x(? if T extends Calculate<A>): U
  y(? if T extends Calculate<B>): V
}

如果对这个问题的答案是“实际上,这些条件将非常少”,那么它似乎不像CalcString | CalcNonString<T>那样获得很多好处。

vtwuwzda

vtwuwzda2#

我没有考虑递归约束 - 对于我的目的,我肯定不需要这些,所以拒绝它们是可以的。这将与编写

type Recursive<T> = Recursive<T> extends { foo : string } ? {foo: T} : number;
//// ^^^^^^^^^ Type alias 'Recursive' circularly references itself.(2456)

一个具体的例子,显然不能重写为仅仅是一个联合,将是类似于:

type ForString<T extends string> = {
  value: T;
  render: (item: T) => Result;
  key?: (value: T) => string; // optional
}

type ForAny<T> = {
  value: T;
  render: (item: T) => Result;
  key: (value: T) => string; // required
}

你不能写

type ForBoth<T> = ForString<T> | ForAny<T>; // Type 'T' does not satisfy the constraint 'string'.(2344)

并且编写

type ForBoth<T extends string> = ForString<T> | ForAny<T>;

破坏了拥有 ForAny 的目的。因此,您可以编写

type ForBoth<T> = T extends string ? ForString<T> : ForAny<T>;

我不能仅仅使用一个非通用的 ForString ,因为还有其他字段也引用了 T ,这可能比 string 更具体(例如,联合、枚举或带标签的模板字面量类型)。或者更一般地,带有具有某些属性或其他约束的对象。

pgvzfuti

pgvzfuti3#

我非常赞成这个功能,尽管可能只是这个功能的子集。目前,解决方案需要切换到 type(而不是 interface),这会破坏其他事物。
我的用例是根据类型参数拥有两个"侧面"。
例如:

interface Arguments<Namespaced extends boolean> {
  ...
  namespace(? if Namespaced !extends true): Namespaced extends true
	? string
	: Namespaced extends false
      ? undefined
      : string | undefined;
}
lb3vh1jj

lb3vh1jj4#

也适用于:

export type MayHaveUsefulFoo<MyFoo> = {
  //  ... some other useful props
  foo(? if MyFoo extends undefined | null | void): MyFoo 
}

而不是

export type MayHaveUsefulFoo<MyFoo> = {
  //  ... some other useful props
} & (MyFoo extends undefined | null | void
  ? { foo?: MyFoo }
  : { foo: MyFoo })
gpnt7bae

gpnt7bae5#

当前的解决方案确实看起来相当丑陋。

export type StoryAnnotations<
  TFramework extends AnyFramework = AnyFramework,
  TArgs = Args,
  TArgsAnnotations extends Partial<TArgs> = Partial<TArgs>
> = BaseAnnotations<TFramework, TArgs> & {
  /**
* Override the display name in the UI (CSF v3)
*/
  name?: StoryName;

  /**
* Override the display name in the UI (CSF v2)
*/
  storyName?: StoryName;

  /**
* Function that is executed after the story is rendered.
*/
  play?: PlayFunction<TFramework, TArgs>;

  /** @deprecated */
  story?: Omit<StoryAnnotations<TFramework, TArgs>, 'story'>;
} & ({} extends TArgsAnnotations ? { args?: TArgsAnnotations } : { args: TArgsAnnotations });

它还迫使您将接口重写为具有交集的类型。

xoefb8l8

xoefb8l86#

我正在寻找一些简单、人类可读且肉眼易懂的参数类型,以便在类型上提供一个可用的参数:
使用你的“避免而不是”,我实现了这个:

export type GlobalApiCallOptions<PARAMS, TYPE, REQBODY> = {
  hooks?: ApiCallHooks<TYPE>
  requestConfig?: AxiosRequestConfig
} & (PARAMS extends undefined | null | void
  ? {}
  : { params: PARAMS })
  & (REQBODY extends undefined | null | void
  ? {}
  : { requestBody: REQBODY })

当然,它已经用于其他目的了,对我来说,这个类型本身是一个函数类型的生成器,
而且我想通过IDE提供更好的自动建议,并防止开发者在某些情况下传递不应该传递的参数。

相关问题