返回

解决 TypeScript 计算属性 Setter 报错的实用指南

vue.js

TypeScript 计算属性 Setter 错误解析

在 TypeScript 项目开发过程中,我们有时会遇到编辑器(例如 VSCode)发出关于计算属性 setter 的误报。一个常见的场景是在使用 Vue 或 Pinia 等状态管理库时,使用 computed 创建带有 setter 的属性。即使属性的赋值实际工作正常,TypeScript 的类型检查器仍会提示错误:“无法分配到 ‘xxx’,因为它是一个只读属性。”(Cannot assign to ‘xxx’ because it is a read-only property.)。理解此类错误的根本原因以及如何有选择地处理它们对于高效开发至关重要。

问题根源

TypeScript 类型检查器会根据类型的定义进行严格的静态分析。在上述场景中, computed 返回的对象会被 TypeScript 推断为一个带有只读属性的接口。这是因为 computed 的默认行为返回只读的计算属性,即使定义了 setter。因此,当你尝试赋值到这个计算属性时,TypeScript 会误判这是一个只读属性,从而抛出类型错误。但是,运行时JavaScript可以正常工作,因为 computed 属性定义了实际的 getter 和 setter 。这种错误属于误报,由TypeScript的类型推导的局限性引起。

解决方案

解决此问题的目标是在保持 TypeScript 类型检查准确性的前提下,抑制或修正编辑器误报。以下方法提供了一些可行选项,请根据项目实际情况选择。

类型断言(Type Assertion)

类型断言允许你覆写 TypeScript 的默认类型推断,明确指定某个变量的类型。在这个情景中,我们可以断言 computed 的结果类型为一个带有 getset 方法的属性类型。这本质上告诉 TypeScript 我们已经了解属性的实际类型。

代码示例:

import { defineStore, ref, computed } from 'pinia';

class MyClass {
  myValue: string;
  constructor(){
     this.myValue = '';
  }
}


export const useSomeStore = defineStore('someStore', () => {
  const myClass = ref(new MyClass());

  const myValue = computed({
    get() {
      return myClass.value.myValue;
    },
    set(value: string) {
      myClass.value.myValue = value;
    }
  }) as { get: () => string; set: (value: string) => void };
    return { myValue }
});

操作步骤:

  1. computed 调用的末尾添加 as { get: () => string; set: (value: string) => void }。这告诉 TypeScript myValue 是一个带有 get 和 set 的属性,类型为字符串。

优点: 直接明了,修改位置清晰。
缺点: 需要开发者对计算属性的类型有了解。

使用类型声明(Type Declarations)

可以通过创建一个类型定义的方式,来清晰地表示一个包含 getter 和 setter 的计算属性,然后将计算属性类型声明为此类型。这样可以有效提升代码的可读性和可维护性,并且更加显式地表明了开发者对属性类型的意图。

代码示例:

import { defineStore, ref, computed, ComputedRef } from 'pinia';

class MyClass {
  myValue: string;
    constructor(){
        this.myValue = '';
    }
}
//定义新的类型, 接收泛型
type WritableComputedRef<T> =  ComputedRef<T> & {
  value: T
}

export const useSomeStore = defineStore('someStore', () => {
  const myClass = ref(new MyClass());

  const myValue = computed({
    get() {
      return myClass.value.myValue;
    },
    set(value: string) {
      myClass.value.myValue = value;
    }
  }) as WritableComputedRef<string>;

   return { myValue }

});

操作步骤:

  1. computed 定义上方或者某个公共的文件定义WritableComputedRef<T>类型。
  2. computed 定义中用 as 断言其类型为 WritableComputedRef<string>

优点: 代码更加清晰易懂, 避免了魔法字符串
缺点: 需要额外的类型定义。

忽略特定错误

如果觉得断言方式仍然繁琐,可以选择使用 // @ts-ignore 注释来临时忽略 TypeScript 报告的错误,这允许直接跳过对特定代码行的类型检查,从而消除编辑器的警告。然而,应谨慎使用此方式。

代码示例:

import { defineStore, ref, computed } from 'pinia';

class MyClass {
  myValue: string;
  constructor(){
    this.myValue = '';
  }
}
export const useSomeStore = defineStore('someStore', () => {
  const myClass = ref(new MyClass());

  const myValue = computed({
    get() {
      return myClass.value.myValue;
    },
    set(value: string) {
      myClass.value.myValue = value;
    }
  });
  return { myValue }
});
const someStore = useSomeStore();
// @ts-ignore
someStore.myValue = "hello";

操作步骤:

  1. 在导致错误的代码行前添加 // @ts-ignore 注释。

优点: 方法快速简单,可直接屏蔽误报。
缺点: 会对代码的健壮性和类型安全性带来潜在风险。 应尽量避免。

安全建议

  • 选择合适的方法: 在项目中根据需求、团队规模、以及维护难度选择最佳的解决方法。建议使用类型断言或类型定义,尽量避免使用// @ts-ignore
  • 理解根本原因: 当 TypeScript 报告类型错误时,要试图理解错误原因。盲目使用 @ts-ignore 不利于培养好的代码习惯。
  • 持续维护类型声明: 如果项目中选择使用类型定义方式,当逻辑变更或者需要新功能的时候,应当维护类型定义使其与代码保持一致。
  • 关注版本更新: TypeScript和各个框架都会有新的版本推出。时刻保持关注,可能旧的问题会在新的版本被修复。
  • 配置tsconfig : 检查tsconfig的设置是否合理,适当调整strict等设置。
  • 代码审查: 团队进行代码审查的时候,仔细检查是否有使用 // @ts-ignore 且没有进行类型修复的代码。
  • 考虑 ESLint 或 TypeScript linter 规则: 某些 linting 工具可以配置规则,以禁止特定 // @ts-ignore 的用法,从而加强代码质量管理。

总之,理解 TypeScript 的类型系统以及它的工作方式对于解决编辑器报错问题非常关键。对于 computed setter 报错,我们既可以通过精确地声明类型修正类型推断,又可以跳过错误提示,但是不建议后者。使用何种方案取决于开发者的实际需求,以及项目的具体情况。选择适合自己的方案才能更高效、安全的进行软件开发。