如何将 Typescript 类型定义为字符串字典但具有一个数字“id”属性

Posted

技术标签:

【中文标题】如何将 Typescript 类型定义为字符串字典但具有一个数字“id”属性【英文标题】:How to define Typescript type as a dictionary of strings but with one numeric "id" property 【发布时间】:2021-11-10 14:42:23 【问题描述】:

现有的 javascript 代码具有“记录”,其中 id 为数字,其他属性为字符串。 尝试定义这种类型:

type t = 
    id: number;
    [key:string]: string

给出错误 2411 id type number not assignable to string

【问题讨论】:

【参考方案1】:

TypeScript 中没有与您想要的结构相对应的特定类型。字符串index signatures 必须适用于每个 属性,即使是像id 这样手动声明的属性。您正在寻找的是类似“rest index signature”或“default property type”之类的东西,GitHub 中有一个公开的建议要求这样做:microsoft/TypeScript#17867。前段时间有一些工作可以实现这一点,但它被搁置了(有关更多信息,请参阅this comment)。所以尚不清楚何时或是否会发生这种情况。


您可以扩大索引签名属性的类型,使其通过联合包含硬编码的属性,例如

type WidenedT = 
    id: number;
    [key: string]: string | number

但是您必须先测试每个动态属性,然后才能将其视为string

function processWidenedT(t: WidenedT) 
    t.id.toFixed(); // okay
    t.random.toUpperCase(); // error
    if (typeof t.random === "string") t.random.toUpperCase(); // okay


最好的方法是,如果您可以重构 JavaScript,使其不会“混合”string-valued 属性包与 number-valued @ 987654335@。例如:

type RefactoredT = 
    id: number;
    props:  [k: string]: string ;

这里idprops 是完全独立的,您不必执行任何复杂的类型逻辑来确定您的属性是number 还是string 值。但这需要对您现有的 JavaScript 进行大量更改,并且可能不可行。

从这里开始,我假设你不能重构你的 JavaScript。但请注意,与即将出现的杂乱内容相比,上面的内容是多么干净:


缺少剩余索引签名的一个常见解决方法是使用intersection type 来绕过索引签名必须应用于每个属性的约束:

type IntersectionT = 
    id: number;
 &  [k: string]: string ;

有点像工作;当给定一个IntersectionT 类型的值时,编译器将id 属性视为number,将任何其他属性视为string

function processT(t: IntersectionT) 
    t.id.toFixed(); // okay
    t.random.toUpperCase(); // okay
    t.id = 1; // okay
    t.random = "hello"; // okay

但这并不是真正的类型安全,因为您在技术上声称id 既是number(根据第一个交叉点成员)又是string(根据第二个交叉点成员)。因此,不幸的是,如果编译器不抱怨,您就无法将对象文字分配给该类型:

t =  id: 1, random: "hello" ; // error!
// Property 'id' is incompatible with index signature.

您必须通过执行Object.assign() 之类的操作来进一步解决这个问题:

const propBag:  [k: string]: string  =  random: "" ;
t = Object.assign( id: 1 , propBag);

但这很烦人,因为大多数用户永远不会想到以这种迂回的方式合成对象。


另一种方法是使用generic 类型来表示您的类型而不是特定类型。考虑编写一个类型 checker,它将 candidate 类型作为输入,当且仅当该候选类型与您所需的结构匹配时才返回兼容的内容:

type VerifyT<T> =  id: number  &  [K in keyof T]: K extends "id" ? unknown : string ;

这将需要一个通用辅助函数,以便您可以推断通用 T 类型,如下所示:

const asT = <T extends VerifyT<T>>(t: T) => t;

现在编译器将允许您使用对象字面量,它会按照您期望的方式检查它们:

asT( id: 1, random: "hello" ); // okay
asT( id: "hello" ); // error! string is not number
asT( id: 1, random: 2 ); // error!  number is not string
asT( id: 1, random: "", thing: "", thang: "" ); // okay

不过,读取这种类型的带有未知键的值有点困难。 id 属性没问题,但其他属性不知道存在,你会得到一个错误:

function processT2<T extends VerifyT<T>>(t: T) 
    t.id.toFixed(); // okay
    t.random.toUpperCase(); // error! random not known to be a property


最后,您可以使用一种混合方法,该方法结合了交集类型和泛型类型的最佳方面。使用泛型类型创建值,使用交集类型读取它们:

function processT3<T extends VerifyT<T>>(t: T): void;
function processT3(t: IntersectionT): void 
    t.id.toFixed();
    if ("random" in t)
        t.random.toUpperCase(); // okay

processT3( id: 1, random: "hello" );

上面是一个overloaded函数,其中调用者看到的是泛型,而实现看到的是交集类型。


Playground link to code

【讨论】:

很好的答案,谢谢。不能更完整,而且内容也很丰富,有点像迷你高级 Typescript 课程。我真的很感激。 非常聪明。我知道应该有函数助手,但我永远不会想出将 [unknown] 类型与 [number] 合并【参考方案2】:

您收到此错误是因为您已将其声明为可索引类型(参考:https://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types),其中字符串是键类型,因此 id 是数字不符合该声明。

在这里很难猜出你的意图,但你可能想要这样的东西:

class t 
  id: number;
  values = new Map<string, string>();

【讨论】:

是的,“递减”一个级别的值将是 new 代码的完美解决方案,但不幸的是不适用于此处。请参阅 jcalz 的回答,非常完整且内容丰富。【参考方案3】:

我也遇到了同样的问题,但将 id 作为字符串返回。

export type Party =  [key: string]: string 

我更喜欢在接收代码上有一个平面类型和 parseInt(id)。

对于我的 API,可能是最简单的方法。

【讨论】:

以上是关于如何将 Typescript 类型定义为字符串字典但具有一个数字“id”属性的主要内容,如果未能解决你的问题,请参考以下文章

如何从 Typescript 中的常量定义字符串文字联合类型

如何仅使用 TypeScript 声明其属性为字符串类型的对象?

如何将字符串类型转换为 Typescript 中的一种对象类型?

如何在 Typescript 中定义正则表达式匹配的字符串类型?

Vue.js + Typescript:如何将@Prop 类型设置为字符串数组?

打字稿:如何将字符串转换为类型