为字符串对象属性定义类型。泛型和显式类型之间的区别

Posted

技术标签:

【中文标题】为字符串对象属性定义类型。泛型和显式类型之间的区别【英文标题】:Defining a type for object properties that are strings. Differences between generic and explicit types 【发布时间】:2020-08-17 06:02:49 【问题描述】:

Playground link here

我想定义一个函数searchText,它接受需要过滤的对象数组、将要搜索的对象属性数组以及要搜索的字符串值。最后,我希望这样的事情成为可能:

type User =  firstName: string, lastName: string, age: number
let users: User[] = [
     firstName: 'John', lastName: 'Smith', age: 22,
     firstName: 'Ted', lastName: 'Johnson', age: 32
]
searchText(users, ['firstName', 'lastName'], 'john') // should find both entries

我想将此函数限制为仅接受有效的属性名称数组。一个有效的数组应该只包含具有字符串类型的属性,因为该函数正在搜索文本。在另一个 SO 问题中,我找到了一种定义类型的方法,该类型只应允许有效值 (https://***.com/a/54520829/1242967)。这种类型定义为

type KeysMatching<T, V> = [K in keyof T]: T[K] extends V ? K : never[keyof T];

使用这种类型,我可以定义一个允许指定“有效数组约束”的函数

function searchTextInUsers(users: User[], stringFields: KeysMatching<User, string>[], text: string): User[]
    return users.filter(user =>
        stringFields.some(field => user[field].toLowerCase().includes(text.toLowerCase()))
    );

这会按预期编译和工作。尝试传递非字符串属性会导致编译错误

searchTextInUsers(users, ['firstName', 'lastName', 'age'], 'john') // ERROR: Type 'string' is not assignable to type '"firstName" | "lastName"'.

但我真正想做的是编写一个适用于任何类型的函数,不仅仅是User,而且令人惊讶的是,仅仅添加一个泛型类型参数不起作用。

function searchText<T>(elements: T[], stringFields: KeysMatching<T, string>[], text: string): T[]
    return elements.filter(element => 
        stringFields.some(field => element[field].toLowerCase().includes(text.toLowerCase())) // Error on this line
    );

编译器显示以下错误

类型 'T[ [K in keyof T] 上不存在属性 'toLowerCase': T[K] 扩展字符串? K:从不; [keyof T]]'

有人可以解释为什么这不起作用以及为什么打字稿在这种情况下没有将类型缩小为字符串?使用泛型类型时,是否有另一种方法可以实现我想要做的事情? Playground link here

【问题讨论】:

【参考方案1】:

简而言之,在这种情况下,打字稿并不像您希望的那样聪明。

在您的searchTextInUsers 函数中,它知道type K = KeysMatching&lt;User, string&gt; 只能是"firstName" | "lastName",并看到这两个键都从User 返回string 属性。但它无法仅从 KeysMatching 类型中弄清楚这一点。

不要向后工作,而是使用允许 typescript 向前移动的泛型。

function searchText<K extends keyof any, T extends Record<K, string>>(elements: T[], stringFields: K[], text: string): T[]
    return elements.filter(element => 
        stringFields.some(field => element[field].toLowerCase().includes(text.toLowerCase()))
    );

这里我们说K 类型表示我们作为stringFields 传入的字符串属性键。我们的元素必须具有所有这些键,并且我们知道对应的值是strings,所以我们说T 必须扩展Record&lt;K, string&gt;

这按预期工作。 searchText(users, ['firstName', 'lastName'], 'john') 很好。但是如果我们尝试传入一个不是字符串属性的属性名称,比如'age',我们会得到一个错误,因为数据数组users 不符合契约age: string

searchText(users, ['firstName', 'age'], 'john')) 给出这个错误:

Argument of type ' firstName: string; lastName: string; age: number; []' is not assignable to parameter of type 'Record<"firstName" | "age", string>[]'.
  Type ' firstName: string; lastName: string; age: number; ' is not assignable to type 'Record<"firstName" | "age", string>'.
    Types of property 'age' are incompatible.
      Type 'number' is not assignable to type 'string'.

TS Playground Link

【讨论】:

以上是关于为字符串对象属性定义类型。泛型和显式类型之间的区别的主要内容,如果未能解决你的问题,请参考以下文章

Oracle sql中的隐式和显式数据类型转换有啥区别

关于隐式转换和显式转换

C#中的类型转换-自定义隐式转换和显式转换

Java学生信息表,Map存储对象,Map使用泛型和增强for循环来做

泛型和枚举

理解Java泛型和类型擦除