首页 > 解决方案 > 如何将 Typescript 类型定义为字符串字典但具有一个数字“id”属性

问题描述

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

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

给出错误 2411 id 类型号不可分配给字符串

标签: typescript

解决方案


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


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

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 id。例如:

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

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

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


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

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

它有点像作品;当给定 type 的值时IntersectionT,编译器将id属性视为 a number,将任何其他属性视为 a string

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

但这并不是真正的类型安全,因为您在技术上声称id它既是 a number(根据第一个交叉点成员)又是 a 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);

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


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

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" });

上面是一个重载函数,调用者看到的是泛型类型,但实现看到的是交集类型。


Playground 代码链接


推荐阅读