首页 > 解决方案 > TypeScript 无法正确检查对象属性

问题描述

我有一个具有字符串和数字属性值的对象。当我想设置某些字段的值时,TypeScript 在使用indexOf检查方法检查属性类型时失败。

这是示例:

type TSomeObject = {
  title: string,
  description: string,
  count: number
}

const someFunction = (myObj: TSomeObject, field: keyof TSomeObject, newValue: string): TSomeObject => {
  if (field === 'title' || field === 'description') {
    myObj[field] = newValue
  }
  return myObj
}

const otherFunction = (myObj: TSomeObject, field: keyof TSomeObject, newValue: string): TSomeObject =>{
  const editableFields = ['title', 'description']
  if (editableFields.indexOf(field) >= 0) {
    // TypeScript error here
    myObj[field] = newValue
  }
  return myObj
}

请参阅TS playground中的此示例。

虽然someFunction工作得很好,otherFunction但因 TypeScript 错误而失败:

类型“字符串”不可分配给类型“从不”。

我知道它与counttype 的属性有关number,但是我检查了我正在编辑哪个字段。我在这里做错了什么?

标签: typescript

解决方案


这里正在发生一些事情。


一个是editableFields推断为string[],这对于您的目的来说太宽了。从 TS3.4 开始,您可以使用const断言来推断更窄的文字类型:

const editableFields = ['title', 'description'] as const;
// const editableFields: readonly ["title", "description"]

NoweditableFields是一个元组,其成员已知为"title""description"


接下来, TypeScript不会将 的结果arr.indexOf(x) >= 0作为. 所以即使测试是, 的类型也不会自动缩小。这只是 TypeScript 的一个限制;并非所有可能的测试方法都可以被编译器检测到。幸运的是,TypeScript 使您能够创建用户定义的类型保护函数。在这种情况下,您可能想要做这样的事情:xtruextypeof arr[number]

declare function arrayContains<T extends U, U>(
  haystack: ReadonlyArray<T>, 
  needle: U
): needle is T;

(我使用ReadonlyArray而不是Array因为它更通用。每个Array都是 aReadonlyArray但反之则不然。是的,这是一个奇怪的名称,因为ReadonlyArray可能不是只读的;也许ReadonlyArray应该是DefinitelyReadableButNotNecessarilyWritableArray并且Array应该是DefinitelyBothReadableAndWritableArray。也许不是。)

那是一个通用的用户定义的类型保护,它接受haystack某种类型的数组Array<T>,以及某种比 更宽needle的类型,如果它返回,编译器会理解它。(它还认为这意味着is not,这可能并不总是可取的。在您的情况下,因为and都将是字符串文字或它们的联合,所以这不是问题。)UTtrueneedle is Tfalseneedle TTU

如果我们有一个实现arrayContains(),我们会将测试更改为:

if (arrayContains(editableFields, field)) {
    myObj[field] = newValue; // no error now
}

所以,让我们实现arrayContains().


接下来是:TypeScript 的标准库的indexOf()类型是这样的:

interface ReadonlyArray<T> {
  indexOf(searchElement: T, fromIndex?: number): number;
}

这意味着needle需要Thaystack. 但是您希望您needle的类型更广泛(您正在查看 a是否在元素string数组中)。"this" | "that"所以 TypeScript 不会让你这样做:

function arrayContains<T extends U, U>(haystack: ReadonlyArray<T>, needle: U): needle is T {
    return haystack.indexOf(needle) >= 0; // error!
    //                      ~~~~~~ <-- U is not assignable to T
}

幸运的是,您可以安全地将 aReadonlyArray<T>扩展为 a ReadonlyArray<U>,因此您可以这样写:

function arrayContains<T extends U, U>(haystack: ReadonlyArray<T>, needle: U): needle is T {
    const widenedHaystack: ReadonlyArray<U> = haystack;
    return widenedHaystack.indexOf(needle) >= 0; 
}

那行得通!相反,您也可以只使用类型断言来使编译器静音,如下所示:

function arrayContains<T extends U, U>(haystack: ReadonlyArray<T>, needle: U): needle is T {
    return haystack.indexOf(needle as T) >= 0; // assert
}

但我通常会尽量避免断言,除非它们是必要的,而在这里它们不是。由你决定。


所以让我们把它们放在一起:

function arrayContains<T extends U, U>(haystack: ReadonlyArray<T>, needle: U): needle is T {
    const widenedHaystack: ReadonlyArray<U> = haystack;
    return widenedHaystack.indexOf(needle) >= 0; 
}

function otherFunction(myObj: TSomeObject, field: keyof TSomeObject, newValue: string): TSomeObject {
    const editableFields = ['title', 'description'] as const;
    if (arrayContains(editableFields, field)) {
        myObj[field] = newValue
    }
    return myObj
}

好的,希望有帮助。祝你好运!

链接到代码


推荐阅读