如何在TypeScript中检查属性是否为只读?

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

How to check if a property is readOnly in TypeScript?

问题

我在此函数的最后一行遇到以下错误:“无法分配给 'ATTRIBUTE_NODE',因为它是只读属性。”

我尝试使用 Object.getOwnPropertyDescriptor 方法来使用守卫子句,但 TypeScript 仍然无法确定我是否总是访问只读属性。我大多数情况下需要访问 "innerText" 属性,但有时也需要访问 "src" 属性来动态获取图像,这就是我使用索引方法的原因。这是否是一种不良实践,还是有解决方法?

function fillData(selector: string, property: string, data: string, parentElem: HTMLElement){
  const targetElem = parentElem.querySelector(`[data-${selector}]`) as HTMLElement
  
  if (!property) return

  targetElem[property as keyof typeof targetElem] = data
}
英文:

I am experiencing the following error in the very last line of this function: "Cannot assign to 'ATTRIBUTE_NODE' because it is a read-only property."

I have tried using the Object.getOwnPropertyDescriptor method to utilize a guard clause but TypeScript still can't determine if I am always accessing a readOnly property or not. I need to access the "innerText" property most of the time but sometimes I also need to access the "src" property to dynamically get an image, that's why I am using the index method. Is this a bad practice or is there a fix?

function fillData(selector: string, property: string, data: string, parentElem: HTMLElement){
  const targetElem = parentElem.querySelector(`[data-${selector}]`) as HTMLElement
  
  if (!property) return

  targetElem[property as keyof typeof targetElem] = data
}

答案1

得分: 1

这是基本上无法由编译器检查的,因为你要变异的DOM元素的类型在编译时不存在,不能被推断 - 所以你必须注释/断言你期望被选择的元素的类型。

有了这个(相当大的)警告,以下是如何做到这一点:

注意:WritableKeys 实用程序是从这里借来的答案这里这里


供参考,以下是代码中使用的类型工具:

type IfEquals<X, Y, A = X, B = never> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? A : B;

type WritableKeys<T> = {
  [P in keyof T]-?:
    IfEquals<
      { [Q in P]: T[P] },
      { -readonly [Q in P]: T[P] },
      P
    >;
}[keyof T];

type KeysByValue<T, V> = keyof {
  [
    K in keyof T as
      T[K] extends V ? K : never
  ]: unknown;
};

通过使用两个受限制的泛型类型参数与你的函数,你可以实现期望的结果:

TS Playground

function fillData<T extends Element, K extends Extract<KeysByValue<T, string>, WritableKeys<T>>>(selector: string, property: K, data: T[K], parentElem: HTMLElement): void {
  const targetElem = parentElem.querySelector<T>(`[data-${selector}]`);
  if (!targetElem) return;
  targetElem

<details>
<summary>英文:</summary>

This is fundamentally not checkable by the compiler because the type of DOM element that you want to mutate doesn&#39;t exist at compile time and can&#39;t be [inferred](https://www.typescriptlang.org/docs/handbook/type-inference.html) —&#160;so you&#39;ll have to annotate/assert the type of element that you expect to be selected by your selector.

With that (quite large) caveat out of the way, here&#39;s how you can do it:

&gt; Note: the `WritableKeys` utility is borrowed from the answers [here](https://stackoverflow.com/a/52473108/438273) and [here](https://stackoverflow.com/a/49579497/438273).

---

For reference, here are the type utilities used in the code below:

```lang-ts
type IfEquals&lt;X, Y, A = X, B = never&gt; =
  (&lt;T&gt;() =&gt; T extends X ? 1 : 2) extends
  (&lt;T&gt;() =&gt; T extends Y ? 1 : 2) ? A : B;

type WritableKeys&lt;T&gt; = {
  [P in keyof T]-?:
    IfEquals&lt;
      { [Q in P]: T[P] },
      { -readonly [Q in P]: T[P] },
      P
    &gt;
}[keyof T];

type KeysByValue&lt;T, V&gt; = keyof {
  [
    K in keyof T as
      T[K] extends V ? K : never
  ]: unknown;
};

By using two constrained generic type parameters with your function, you can achieve the desired result:

TS Playground

function fillData&lt;
  T extends Element,
  K extends Extract&lt;KeysByValue&lt;T, string&gt;, WritableKeys&lt;T&gt;&gt;
&gt;(selector: string, property: K, data: T[K], parentElem: HTMLElement): void {
  const targetElem = parentElem.querySelector&lt;T&gt;(`[data-${selector}]`);
  if (!targetElem) return;
  targetElem[property] = data;
}

declare const parentElement: HTMLElement;

fillData(&quot;key&quot;, &quot;className&quot;, &quot;my-class&quot;, parentElement); // Ok
// no generic used, so inferred as Element - &quot;className&quot; exists on Element

fillData(&quot;key&quot;, &quot;src&quot;, &quot;ok&quot;, parentElement); // Error
//              ~~~~~
// no generic used, so inferred as Element - &quot;src&quot; doesn&#39;t exist on Element

fillData&lt;HTMLImageElement, &quot;src&quot;&gt;(&quot;key&quot;, &quot;src&quot;, &quot;img.jpg&quot;, parentElement); // Ok
//       ^^^^^^^^^^^^^^^^  ^^^^^
// but it&#39;s repetitive: an explicit generic type must be provided for the element AND the property

fillData&lt;HTMLImageElement, &quot;alt&quot;&gt;(&quot;key&quot;, &quot;src&quot;, &quot;img.jpg&quot;, parentElement); // Error
//                                       ~~~~~
// property value doesn&#39;t match the explicitly provided generic

fillData&lt;HTMLImageElement, &quot;loading&quot;&gt;(&quot;key&quot;, &quot;loading&quot;, &quot;ok&quot;, parentElement); // Error (expected &#128077;)
//                                                      ~~~~
// Argument of type &#39;&quot;ok&quot;&#39; is not assignable to parameter of type &#39;&quot;eager&quot; | &quot;lazy&quot;&#39;.(2345)

fillData&lt;HTMLDivElement, &quot;src&quot;&gt;(&quot;key&quot;, &quot;src&quot;, &quot;my-class&quot;, parentElement); // Error (expected &#128077;)
//                       ~~~~~
// &quot;src&quot; doesn&#39;t exist on HTMLDivElement

However, as you can see in the commented usage examples, it's not very DRY because it requires repeating the property twice — once as a type parameter and once as the actual value. TypeScript theoretically has the capability of partial inference, but it is not yet an implemented feature. For more info, see this GitHub issue: microsoft/TypeScript#10571 - Allow skipping some generics when calling a function with multiple generics

So, while I think this answers your question... can it be improved? I think an alternate pattern could look something like this:

TS Playground

function selectByData&lt;T extends Element&gt;(
  selector: string,
  parent: ParentNode = document,
): {
  set: &lt;K extends Extract&lt;KeysByValue&lt;T, string&gt;, WritableKeys&lt;T&gt;&gt;&gt;(
    property: K,
    value: T[K],
  ) =&gt; void;
} {
  const element = parent.querySelector&lt;T&gt;(`[data-${selector}]`);
  return { set: element ? ((k, v) =&gt; element[k] = v) : (() =&gt; {}) };
}

declare const parentElement: HTMLElement;

selectByData(&quot;key&quot;, parentElement).set(&quot;className&quot;, &quot;my-class&quot;); // Ok
// no generic used, so inferred as Element - &quot;className&quot; exists on Element

selectByData(&quot;key&quot;, parentElement).set(&quot;src&quot;, &quot;ok&quot;); // Error
//                                     ~~~~~
// no generic used, so inferred as Element - &quot;src&quot; doesn&#39;t exist on Element

selectByData&lt;HTMLImageElement&gt;(&quot;key&quot;, parentElement).set(&quot;src&quot;, &quot;img.jpg&quot;); // Ok
//           ^^^^^^^^^^^^^^^^
// now, only the generic type for the element must be provided

selectByData&lt;HTMLImageElement&gt;(&quot;key&quot;, parentElement).set(&quot;loading&quot;, &quot;ok&quot;); // Error (expected &#128077;)
//                                                                  ~~~~
// Argument of type &#39;&quot;ok&quot;&#39; is not assignable to parameter of type &#39;&quot;eager&quot; | &quot;lazy&quot;&#39;.(2345)

selectByData&lt;HTMLDivElement&gt;(&quot;key&quot;, parentElement).set(&quot;src&quot;, &quot;my-class&quot;); // Error (expected &#128077;)
//                                                     ~~~~~
// &quot;src&quot; doesn&#39;t exist on HTMLDivElement

selectByData&lt;HTMLDivElement&gt;(&quot;key&quot;, parentElement).set(&quot;tagName&quot;, &quot;my-class&quot;); // Error (expected &#128077;)
//                                                     ~~~~~~~~~
// &quot;tagName&quot; exists on HTMLDivElement, but isn&#39;t writable

In this alternate version, the work is done in two steps:

The initial function is invoked using two arguments:

  1. the partial data attribute name as the selector, and
  2. optionally, the parent node (if not provided, document is used as the default)

The return value of that function is an object with a set method which can be invoked using the

  1. writable property name, and
  2. corresponding value.

If the child element was not found in the first invocation, the setter function that's returned is simply a no-op — you can call it in the same way you would if the element existed, but it will have no effect.

huangapple
  • 本文由 发表于 2023年2月24日 10:32:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/75552136.html
匿名

发表评论

匿名网友

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

确定