如何使用索引签名声明打字稿类字段



type TField = {
field1: string;
field2: boolean;
field3: TMyCustom;
}
class Test1 {
// I want to declare class field with this index signature
[key: keyof TField]: TField[typeof key]
// I don't want write field one by one
// field1: string;
// field2: boolean;
// field3: TMyCustom;
constructor() {
// But TS Can't infer from index signature
// Error : Property 'field1' does not exist on type 'Test1'
this.field1 = 'initialField1'
// Error : Property 'field2' does not exist on type 'Test1'
this.field2 = true;
// Error : Property 'field3' does not exist on type 'Test1'
this.field3 = '';
}
}

我想使用打字稿的索引签名声明类字段。

在上面的例子中,我在 TField 旁边声明了类 Test1,以简化我的情况。

但是在我的项目代码中,我应该从大量文件中导入 TField。所以我不能一一声明 Test1 的字段。

我不确定我写 ts 索引签名是错误的还是 ts 不支持这种类型。

-----------问题已添加


type TField = {
field1: string;
field2: boolean;
field3: TMyCustom;
}
class Test0 {
// Previously, I declare "fields" property type of TField
fields: TField
// But now, I want to declare and access field1,2,3 at the class property level,
// not inside of the "fields" property.
constructor() {
this.fields = {
field1: 'initial field1',
field2: true,
field3: {some: 'object'} as TMyCustom,
}
}
}

在我尝试转换为索引签名之前,我的代码看起来像 Test0 类。

我知道你对"为什么你声明"字段"属性并将字段 1,2,3 嵌套在其中,而不仅仅是将字段 1,2,3 声明为类属性感到好奇?但这与项目结构有关。所以请把它放在一边。

由于字段 1,2,3包含在字段属性中,因此我应该像这样评估字段 1,2,3test0Instance.fields.field1

所以我写了这个问题,我是否可以使用索引签名来声明类字段。

不幸的是,索引签名不会做你想要的。 索引签名可以让您声明classinterface的行为类似于具有任意string键的字典,或者类似于具有任意number(类似)键的数组,从 TypeScript 4.4 开始,您可以使用任意symbol键甚至"模式"模板文字,如`hello-${string}`. 但是你不能像keyof TField那样给出一组有限的字符串文字键。即使你可以这样做,编译器也不会让你单独映射每个键;所有键将具有相同的值类型。 这不是你想要的。

您似乎想说class具有映射类型的键,形式为[K in keyof TField]: TField[K]. 但映射类型不允许成为interfaceclasses 的一部分。 它们是独立类型。

没有内置的解决方案可以满足您的需求。 在另一个宇宙中,TypeScript 可能允许你这样做:

class AlternateUniverseTest implements TField {  
constructor(tfield: TField) {
this.field1 = tfield.field1;
this.field2 = tfield.field2;
this.field3 = tfield.field3;
}
}

implements TField或赋值this.field1 = ...都会让编译器知道AlternateUniverseTest具有与TField相同的字段。 但不幸的是,implements TField并没有改变编译器推断存在哪些属性或其类型的方式(请参阅 microsoft/TypeScript#32082 和许多链接的问题);编译器在任何情况下都不允许你省略属性声明(参见 microsoft/TypeScript#766)。

这意味着,在这个宇宙中,使用这个版本的 TypeScript,你需要在类主体中单独声明各个属性。 或者你需要解决它。


一种解决方法是创建一个泛型类工厂帮助程序函数。你给它指定一个对象类型T,它返回一个形式为{ new (init: T): T}的类构造函数(这是一个构造签名):

function Wrapper<T extends object>(): new (init: T) => T {
return class {
constructor(init: T) {
Object.assign(this, init);
}
} as any;
}

请注意,实现中的class表达式根本不具有任何属性。 我们知道在运行时它将具有init中的任何属性,因为当您将this作为第一个参数传递时,这就是Object.assign()所做的。 但是编译器看不到它。 这对我们来说没关系,我们可以使用as any类型断言来告诉编译器不要担心它。 我们最关心的是当您致电Wrapper()时会发生什么:

class Test extends Wrapper<TField>() {
otherProp: number;
constructor(tfield: TField) {
super(tfield)
this.field1 // <-- this exists now, no error
this.otherProp = 100;
}
}

通过使Test成为Wrapper<TField>()的子类,我们有一个已经知道field1field2field3以及它们的类型是什么的类体。 我们必须调用super并传递给它一个有效的TField,在这种情况下,我们从构造调用中获得(但如果需要,您可以对其进行硬编码)。 您也可以以正常方式添加其他属性。

让我们确保这有效:

const test = new Test({
field1: "hello",
field2: true,
field3: ""
})
console.log(test.field1.toUpperCase()) // HELLO
console.log(test.otherProp.toFixed(2)) // 100.00

看起来不错。 编译器很高兴,希望您也很高兴。 这是一种解决方法,因此可能存在警告。 例如,如果你不希望Test成为任何东西的子类(或者如果它需要成为其他东西的子类),那么你将需要忘记这种方法,或者至少修改它。

操场链接到代码

最新更新