在 TypeScript 中声明动态添加的类属性

Posted

技术标签:

【中文标题】在 TypeScript 中声明动态添加的类属性【英文标题】:Declare dynamically added class properties in TypeScript 【发布时间】:2017-04-23 15:45:49 【问题描述】:

我想在不知道 TypeScript 中的属性名称、值和值类型的情况下将属性分配给类的实例。假设我们有以下example.ts 脚本:

// This could be a server response and could look totally diffent another time...
const someJson:string = ' "foo": "bar", "bar": "baz" '

class MyClass 
  someProperty:boolean

  constructor( json:string ) 
    const parsedJson:any = JSON.parse( json )

    Object.keys( parsedJson ).forEach(
      ( key:string ) => 
        this[ key ] = parsedJson[ key ]
      
    )

    this['someProperty'] = true
  


const myInstance = new MyClass( someJson )

// Works fine, logs `true`.
console.log( myInstance.someProperty )

// Error: Property 'foo' does not exist on type 'MyClass'.
console.log( myInstance.foo )

// Error: Property 'bar' does not exist on type 'MyClass'.
console.log( myInstance.bar )

如何确保 TypeScript 编译器不会抱怨动态添加的属性,而是将它们作为任何类型的 "key": value 对来处理。我仍然希望tsc 确保myInstance.someProperty 必须是boolean 类型,但我希望能够获得myInstance.whatever,即使它没有被定义而不会遇到编译器错误。

我没有找到任何让我明白这一点的文档。也许是因为我不是以英语为母语的人。所以请保持答案简单。

编辑:

我记得有类似下面的东西,但我从来没有让它工作:

interface IMyClass 
  [name:string]: any

【问题讨论】:

【参考方案1】:

问题是您在运行时添加新属性,而编译器无法知道这一点。

如果您事先知道属性名称,那么您可以这样做:

type Json = 
    foo: string;
    bar: string;


...

const myInstance = new MyClass(someJson) as MyClass & Json;
console.log(myInstance.foo) // no error

编辑

如果你事先不知道属性,那么你不能这样做:

console.log(myInstance.foo);

因为你知道 foo 是接收到的 json 的一部分,你可能会有类似的东西:

let key = getKeySomehow();
console.log(myInstance[key]);

这应该可以在编译器没有错误的情况下工作,唯一的问题是编译器不知道返回值的类型,它将是any

所以你可以这样做:

const myInstance = new MyClass(someJson) as MyClass &  [key: string]: string ;
let foo = myInstance["foo"]; // type of foo is string
let someProperty = myInstance["someProperty"]; // type of someProperty is boolean

第二次编辑

因为你知道道具,但不是在课堂上,你可以这样做:

type ExtendedProperties<T> =  [P in keyof T]: T[P] ;
function MyClassFactory<T>(json: string): MyClass & ExtendedProperties<T> 
    return new MyClass(json) as MyClass & ExtendedProperties<T>;

那么你只需像这样使用它:

type Json = 
    foo: string;
    bar: string;
;
const myInstance = MyClassFactory<Json>(someJson);

请注意,这仅适用于 typescript 2.1 及更高版本。

【讨论】:

有趣的答案。但是我没有办法提前知道这些属性。我认为在运行时扩展一个具有未知属性名称的对象对于纯 javascript 来说不应该那么奇特,所以我想知道为什么这是 TypeScript 中的问题。 所以唯一的方法是使用myInstance.foo 而不是myInstance['foo']myClass 转换为any,对吗? 如果您使用myInstance.foo,则意味着您提前知道foo将在json中,因此您可以使用我在原始答案中显示的方法。如果你不知道它会在那里,那么你怎么能在你的代码中拥有它呢? 我明白了...谢谢您的解释。我的代码只是我当前正在处理的节点模块的一个抽象示例,它应该在实例化时加载内容。因此,当我在另一个文件中导入和使用它时,我知道类中有哪些属性,但类本身不知道它将分配给它的属性和类型。 答案没有解决问题。 Typescript 对此没有很好的答案。但我投了赞成票,因为似乎没有更好的选择。【参考方案2】:

如果您想在实例化时通过对象动态添加类属性,并且该对象可以使用类型信息,那么您可以通过这种方式很好地获得完整的类型安全性(只要您不介意使用静态工厂方法):

class Augmentable 
 constructor(augment: any = ) 
   Object.assign(this, augment)
 
 static create<T extends typeof Augmentable, U>(this: T, augment?: U) 
   return new this(augment) as InstanceType<T> & U
 

这是使用(假的)this parameter 来推断类的构造函数类型。然后它构造实例,并将其转换为实例类型(使用 InstanceType 实用程序类型)和您传递给方法的 props 的推断类型的联合。

(我们可以直接转换为Augmentable &amp; U,但是这种方式允许我们扩展类。)

示例

增强基本属性:

const hasIdProp = Augmentable.create( id: 123 )
hasIdProp.id // number

用方法增强:

const withAddedMethod = Augmentable.create(
  sayHello() 
    return 'Hello World!'
  
)


withAddedMethod.sayHello() // Properly typed, with signature and return value

扩展和扩充,在方法扩充中使用this 访问:

class Bob extends Augmentable 
  name = 'Bob'
  override = 'Set from class definition'
  checkOverrideFromDefinition() 
    return this.override
  


interface BobAugment 
  whatToSay: string
  override: string
  sayHelloTo(to: string): void
  checkOverrideFromAugment(): string


const bobAugment: BobAugment = 
  whatToSay: 'hello',
  override: 'Set from augment'

  sayHelloTo(this: Bob & BobAugment, to: string) 
    // Let's combine a class parameter, augment parameter, and a function parameter!
    return `$this.name says '$this.whatToSay' to $to!`
  ,

  checkOverrideFromAugment(this: Bob & BobAugment) 
    return this.override
  


const bob = Bob.create(bobAugment) // Typed as Bob & BobAugment
bob.sayHelloTo('Alice') // "Bob says 'hello' to Alice!"

// Since extended class constructors always run after parent constructors,
// you cannot override a class-set parameter with an augment, no matter
// from where you are checking it.
bob.checkOverrideFromAugment() // "Set from class definition"
bob.checkOverrideFromDefinition() // "Set from class definition"

限制

增强属性实际上并不是类的一部分,因此您不能扩展包含这些增强的类。对于某些用例来说,这可能是一个功能,其中扩充是临时添加,并不意味着修改原型层次结构

.create() 添加非增强参数也并不容易,但是一个简单的解决方法是简单地利用增强功能来完成与额外参数相同的事情。

【讨论】:

【参考方案3】:

您可以将index signature 添加到您的班级:

class MyClass 
  [index: string]: any; //index signature

  someProperty:boolean

  constructor( json:string ) 
    const parsedJson:any = JSON.parse( json )

    Object.keys( parsedJson ).forEach(
      ( key:string ) => 
        this[ key ] = parsedJson[ key ]
      
    )

    this['someProperty'] = true
  

【讨论】:

以上是关于在 TypeScript 中声明动态添加的类属性的主要内容,如果未能解决你的问题,请参考以下文章

在 TypeScript 中动态创建属性

与接口同名的 TypeScript 类

如何将数组作为对象属性添加到 PHP 扩展中声明的类?

TypeScript给接口添加任意属性

Typescript - 如何访问通过命名空间声明的类?

如何在 TypeScript 类中正确声明计算属性名称?