TypeScrip:仅针对未定义的所有属性的密钥索引签名 [英] TypeScript: key index signature only for all properties that are not defined

查看:12
本文介绍了TypeScrip:仅针对未定义的所有属性的密钥索引签名的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经遇到过多种情况,如果能做到以下几点会更好(非常抽象):

export interface FilterItem {
  [key: string]: string | undefined;
  stringArray?: string[];
}

只有这样才会引发错误,因为string[]不可分配给string,这是有意义的。

我只是想知道是否有可能,如果可能,如何创建一个接口,该接口具有一些定义的属性,如stringArray,这些属性可能不在键索引签名之后。因此,键索引签名仅用于定义每隔属性的类型。

可以吗?

推荐答案

这是一个未解决的问题;请参阅microsoft/TypeScript#17687。如果您想看到这一点的实现,您可能会想转到那个问题,并给它一个👍,但我没有任何感觉它正在积极地工作。(不久前,有人在一些功能上做了一些工作,这些功能本来可以实现这一点,但看起来这些功能没有取得进展。

目前只有以下解决方法:


其他答案中建议的交叉技术可能足以满足您的用例,但它们不是完全类型安全或一致的。虽然type FilterItemIntersection = { [key: string]: string | undefined; } & { stringArray?: string[]; }不会立即导致编译器错误,但结果类型似乎要求stringArray属性同时为string | undefinedstring[] | undefined,这是一个与undefined等价的类型,而不是您想要的类型。幸运的是,您可以从FilterItemIntersection类型的现有值中读取/写入stringArray属性,编译器会将其视为string[] | undefined而不是undefined

function manipulateExistingValue(val: FilterItemIntersection) {
    if (val.foo) {
        console.log(val.foo.toUpperCase()); // okay        
    }
    val.foo = ""; // okay

    if (val.stringArray) {
        console.log(val.stringArray.map(x => x.toUpperCase()).join(",")); // okay
    }
    val.stringArray = [""] // okay
}

但直接为该类型赋值可能会出现错误:

manipulateExistingValue({ stringArray: ["oops"] }); // error!     
// -------------------> ~~~~~~~~~~~~~~~~~~~~~~~~~
// Property 'stringArray' is incompatible with index signature.

这将迫使您跳转以获得该类型的值:

const hoop1: FilterItemIntersection = 
  { stringArray: ["okay"] } as FilterItemIntersection; // assert
const hoop2: FilterItemIntersection = {}; 
hoop2.stringArray = ["okay"]; // multiple statements

另一种解决方法是将您的类型表示为generic,而不是具体的。将属性键表示为某种联合类型K extends PropertyKey,如下所示:

type FilterItemGeneric<K extends PropertyKey> = 
  { [P in K]?: K extends "stringArray" ? string[] : string };

获取此类型的值需要手动注释和指定K,或者使用帮助器函数为您推断,如下所示:

const filterItemGeneric = 
  asFilterItemGeneric({ stringArray: ["okay"], otherVal: "" }); // okay
asFilterItemGeneric({ stringArray: ["okay"], otherVal: ["oops"] }); // error!
// string[] is not string ---------------->  ~~~~~~~~
asFilterItemGeneric({ stringArray: "oops", otherVal: "okay" }); // error!
// string≠string[] -> ~~~~~~~~~~~

这正是您想要的,但不幸的是,如果K是未指定的泛型:

,则它比交集版本更难操作此类型的值
function manipulateGeneric<K extends PropertyKey>(val: FilterItemGeneric<K>) {
    val.foo; // error! no index signature
    if ("foo" in val) {
        val.foo // error! can't narrow generic
    }
    val.stringArray; // error! not necessarily present
}

可以将这些解决方法组合在一起,使您在创建和检查具有已知键的值时使用泛型版本,但在操作具有未知键的值时使用交集版本:

const filterItem = asFilterItemGeneric({ stringArray: [""], otherVal: "" }); // okay
function manipulate<K extends PropertyKey>(_val: FilterItemGeneric<K>) {
    const val: FilterItemIntersection = _val; // succeeds
    if (val.otherVal) {
        console.log(val.otherVal.toUpperCase());
    }
    if (val.stringArray) {
        console.log(val.stringArray.map(x => x.toUpperCase()).join(","));
    }
}

但支持一下,最适合打字的答案是从一开始就不要使用这样的结构。如果可能,请切换到类似以下内容,这样您的索引签名可以保持不受不兼容的值的影响:

interface FilterItemTSFriendly {
    stringArray?: string[],
    otherItems?: { [k: string]: string | undefined }
}
const filterItemFriendly: FilterItemTSFriendly = 
  { stringArray: [""], otherItems: { otherVal: "" } }; // okay
function manipulateFriendly(val: FilterItemTSFriendly) {
    if (val.stringArray) {
        console.log(val.stringArray.map(x => x.toUpperCase()).join(","));
    }
    if (val.otherItems?.otherVal) {
        console.log(val.otherItems.otherVal.toUpperCase());
    }
}

这不需要技巧、交集、泛型或泪水。如果可能的话,这就是我的建议。


好的,希望这会有帮助;祝你好运!

Playground link

这篇关于TypeScrip:仅针对未定义的所有属性的密钥索引签名的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆