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

Fre*_*red 16 typescript

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

type t = {
    id: number;
    [key:string]: string
}
Run Code Online (Sandbox Code Playgroud)

给出错误 2411 id 类型编号无法分配给字符串

jca*_*alz 23

TypeScript 中没有与您所需的结构相对应的特定类型。字符串索引签名必须适用于每个属性,甚至是手动声明的属性,例如id. 您正在寻找的是“剩余索引签名”或“默认属性类型”之类的东西,并且 GitHub 中有一个公开建议要求这样做:microsoft/TypeScript#17867。不久前,已经完成了一些工作来实现这一点,但它被搁置了(有关更多信息,请参阅此评论)。因此,尚不清楚这种情况何时或是否会发生。


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

type WidenedT = {
    id: number;
    [key: string]: string | number
}
Run Code Online (Sandbox Code Playgroud)

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

function processWidenedT(t: WidenedT) {
    t.id.toFixed(); // okay
    t.random.toUpperCase(); // error
    if (typeof t.random === "string") t.random.toUpperCase(); // okay
}
Run Code Online (Sandbox Code Playgroud)

继续这里的最佳方法是,如果您可以重构您的 JavaScript,以便它不会将string-valued 属性包与number-valued “混合” id。例如:

type RefactoredT = {
    id: number;
    props: { [k: string]: string };
}
Run Code Online (Sandbox Code Playgroud)

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

从现在开始,我假设您无法重构 JavaScript。但请注意,与即将出现的混乱内容相比,上面的内容是多么干净:


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

type IntersectionT = {
    id: number;
} & { [k: string]: string };
Run Code Online (Sandbox Code Playgroud)

这有点像作品;当给定 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
}
Run Code Online (Sandbox Code Playgroud)

但不幸的是,如果编译器不抱怨,您就无法将对象文字分配给该类型:

t = { id: 1, random: "hello" }; // error!
// Property 'id' is incompatible with index signature.
Run Code Online (Sandbox Code Playgroud)

您必须通过执行以下操作进一步解决该问题Object.assign()

const propBag: { [k: string]: string } = { random: "" };
t = Object.assign({ id: 1 }, propBag);
Run Code Online (Sandbox Code Playgroud)

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


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

type VerifyT<T> = { id: number } & { [K in keyof T]: K extends "id" ? unknown : string };
Run Code Online (Sandbox Code Playgroud)

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

const asT = <T extends VerifyT<T>>(t: T) => t;
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

不过,使用未知键读取这种类型的值有点困难。该id属性很好,但其他属性将不知道存在,并且您会收到错误:

function processT2<T extends VerifyT<T>>(t: T) {
    t.id.toFixed(); // okay
    t.random.toUpperCase(); // error! random not known to be a property
}
Run Code Online (Sandbox Code Playgroud)

最后,您可以使用混合方法,将交集和泛型类型的最佳方面结合起来。使用泛型类型创建值,并使用交集类型读取它们:

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" });
Run Code Online (Sandbox Code Playgroud)

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


Playground 代码链接