网站搜索

使用 TypeScript 明确定义对象类型


我正在将我的小型库从 JavaScript 转换为 TypeScript,并且我那里有一个函数

function create(declarations: Declarations) {

现在声明是一个对象,其键可以有两种类型:

  • 如果键是以下字符串之一:onMemeber/onCollection,那么值应为数字
  • 对于任何其他字符串键,值应为字符串

使用 TypeScript 强制执行这是否可能?我应该如何定义我的 Declarations 接口?

答案#1

在 TypeScript 中没有具体的类型可以表示你的 Declarations 结构。

我会称这个总体概念为“默认属性”类型。(请求这个功能的 GitHub issue 是 microsoft/TypeScript#17867)你希望特定属性属于一种类型,然后其他任何属性都“默认”属于某种不兼容的类型。这就像是一个索引签名,但没有要求所有属性必须可赋值给它的约束。

(为了明确说明,索引签名不能被使用:

type BadDeclarations = {
    onMember: number, // error! number not assignable to string
    onCollection: number, // error! number not assignable to string
    [k: string]: string
};

索引签名 [k: string]: string 意味着 每个 属性都必须可以赋值为 string,即使是 onMember 和 onCollection 也是如此。要创建一个真正可用的索引签名,你需要将属性类型从 string 扩展为 string | number,这可能对你来说并不可行。

曾经有一些拉取请求可以使这成为可能,但看起来它们在短时间内不会成为语言的一部分。

在 TypeScript 中,如果没有适用的具体类型,你可以使用以某种方式受约束的泛型类型。下面是我如何让 Declarations 成为泛型的方式:

type Declarations<T> = {
    [K in keyof T]: K extends 'onMember' | 'onCollection' ? number : string
};

这里是 create() 的签名

function create<T extends Declarations<T>>(declarations: T) {
}

你可以看到,declarations 参数的类型是 T,它被约束为 Declarations<T>。 这个自引用约束确保了对于 declarations 的每个属性 K,它的类型将是 K extends 'onMember' | 'onCollection' ? number : string,这是一个条件类型,它相当直接地翻译了你想要的结构。

让我们看看是否有效:

create({
    onCollection: 1,
    onMember: 2,
    randomOtherThing: "hey"
}); // okay

create({
    onCollection: "oops", // error, string is not assignable to number
    onMember: 2,
    otherKey: "hey",
    somethingBad: 123, // error! number is not assignable to string
})

那对我来说看起来合理。

当然,使用泛型类型并非没有一些烦恼;突然之间,你想用 Declarations 的每个值或函数现在都需要是泛型的。所以你不能写 const foo: Declarations={...}。你需要改成 const foo: Declarations<{onCollection: number, foo: string}>={onCollection: 1, foo: ""}。这足够让人烦恼,你可能会想使用一个辅助函数来让类型自动推断,而不是手动注解:

// helper function
const asDeclarations = <T extends Declarations<T>>(d: T): Declarations<T> => d;

const foo = asDeclarations({ onCollection: 1, foo: "a" });
/* const foo: Declarations<{
    onCollection: number;
    foo: string;
}>*/

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

代码链接

答案#2

似乎这是不可能做到的,在《TypeScript 深入解析》一书中有一个专门讨论这个的章节。虽然你可以声明一个类型,但实际上你无法在 TS 中使用它创建对象:

// No error on this type declaration:
type Declarations = {
    [key: string]: string;
} & {
    onMember: number;
    onCollection: number;
}

// Error does appear here indicating type `number` is not assignable to type `string`.
const declarations: Declarations = {
    onMember: 0,
    onCollection: 0,
    other: 'Is a string'
}

TypeScript Playground 链接。