如何在TypeScript中动态地为对象分配属性?

如果我想以编程方式将属性分配给Javascript中的对象,我会这样做:

var obj = {};
obj.prop = "value";

但是在TypeScript中,这会产生一个错误:

属性'prop'在类型'{}'的值上不存在

我应该如何将任何新属性分配给TypeScript中的对象?

769184 次浏览

尽管编译器抱怨它仍然应该根据您的需要输出它。但是,这将起作用。

const s = {};
s['prop'] = true;

您可以添加此声明以使警告保持沉默。

declare var obj: any;

所有在一个去:

  var obj:any = {}
obj.prop = 5;

我倾向于把any放在另一边,即var foo:IFoo = <any>{};,所以这样的东西仍然是类型安全的:

interface IFoo{
bar:string;
baz:string;
boo:string;
}


// How I tend to intialize
var foo:IFoo = <any>{};


foo.bar = "asdf";
foo.baz = "boo";
foo.boo = "boo";


// the following is an error,
// so you haven't lost type safety
foo.bar = 123;

或者,您可以将这些属性标记为可选:

interface IFoo{
bar?:string;
baz?:string;
boo?:string;
}


// Now your simple initialization works
var foo:IFoo = {};

在线试用

将任何新属性存储在任何类型的对象上,方法是将其类型转换为'any':

var extend = <any>myObject;
extend.NewProperty = anotherObject;

稍后,您可以通过将扩展对象转换回'any'来检索它:

var extendedObject = <any>myObject;
var anotherObject = <AnotherObjectType>extendedObject.NewProperty;

当您的对象具有特定类型时,此解决方案很有用。就像在将对象获取到其他源时一样。

let user: User = new User();
(user as any).otherProperty = 'hello';
//user did not lose its type here.

最好的做法是使用安全打字,我建议你:

interface customObject extends MyObject {
newProp: string;
newProp2: number;
}

要保留以前的类型,请将对象临时强制转换为任何类型

  var obj = {}
(<any>obj).prop = 5;

新的动态属性仅在您使用强制转换时可用:

  var a = obj.prop; ==> Will generate a compiler error
var b = (<any>obj).prop; ==> Will assign 5 to b with no error;

对此的另一个选择是将属性作为集合访问:

var obj = {};
obj['prop'] = "value";

在TypeScript中动态为对象分配属性。

要做到这一点,你只需要像这样使用打字脚本接口:

interface IValue {
prop1: string;
prop2: string;
}


interface IType {
[code: string]: IValue;
}

你可以那样使用它

var obj: IType = {};
obj['code1'] = {
prop1: 'prop 1 value',
prop2: 'prop 2 value'
};

索引类型

可以将obj表示为any,但这违背了使用打字稿的全部目的。obj = {}暗示obj是一个Object。将其标记为any是没有意义的。为了实现所需的一致性,可以定义如下接口。

interface LooseObject {
[key: string]: any
}


var obj: LooseObject = {};

或使其紧凑:

var obj: {[k: string]: any} = {};

LooseObject可以接受任何字符串作为键和any类型作为值的字段。

obj.prop = "value";
obj.prop2 = 88;

此解决方案的真正优雅之处在于您可以在界面中包含类型安全字段。

interface MyType {
typesafeProp1?: number,
requiredProp1: string,
[key: string]: any
}


var obj: MyType ;
obj = { requiredProp1: "foo"}; // valid
obj = {} // error. 'requiredProp1' is missing
obj.typesafeProp1 = "bar" // error. typesafeProp1 should be a number


obj.prop = "value";
obj.prop2 = 88;

Record<Keys,Type>实用程序类型

更新(2020年8月):@transang在评论中提到了这一点

Record<Keys,Type>是打字稿中的实用程序类型。对于不知道属性名称的键值对,它是一个更干净的替代方案。 值得注意的是,Record<Keys,Type>{[k: Keys]: Type}的命名别名,其中KeysType是泛型。 这是值得一提的

为了比较,

var obj: {[k: string]: any} = {};

成为

var obj: Record<string,any> = {}

MyType现在可以通过扩展记录类型来定义

interface MyType extends Record<string,any> {
typesafeProp1?: number,
requiredProp1: string,
}

虽然这回答了最初的问题,但@GreeneCreations的答案这里可能会提供如何解决问题的另一个视角。

要保证类型是Object(即键值对),请使用:

const obj: {[x: string]: any} = {}
obj.prop = 'cool beans'

如果您使用的是TypeScript,大概您想使用类型安全;在这种情况下,裸对象和“any”会被反标。

最好不要使用Object或{},而是使用一些命名类型;或者您可能使用具有特定类型的API,您需要使用自己的字段进行扩展。我发现这是有效的:

class Given { ... }  // API specified fields; or maybe it's just Object {}


interface PropAble extends Given {
props?: string;  // you can cast any Given to this and set .props
// '?' indicates that the field is optional
}
let g:Given = getTheGivenObject();
(g as PropAble).props = "value for my new field";


// to avoid constantly casting:
let k = getTheGivenObject() as PropAble;
k.props = "value for props";

可以通过以下方式将成员添加到现有对象

  1. 拓宽类型(读取:扩展/专门化接口)
  2. 将原始对象转换为扩展类型
  3. 将成员添加到对象
interface IEnhancedPromise<T> extends Promise<T> {
sayHello(): void;
}


const p = Promise.resolve("Peter");


const enhancedPromise = p as IEnhancedPromise<string>;


enhancedPromise.sayHello = () => enhancedPromise.then(value => console.info("Hello " + value));


// eventually prints "Hello Peter"
enhancedPromise.sayHello();

案例1:

var car = {type: "BMW", model: "i8", color: "white"};
car['owner'] = "ibrahim"; // You can add a property:

案例2:

var car:any = {type: "BMW", model: "i8", color: "white"};
car.owner = "ibrahim"; // You can set a property: use any type

我很惊讶没有一个答案引用Object.assign,因为每当我想到JavaScript中的“组合”时,我都会使用这种技术。

它在TypeScript中按预期工作:

interface IExisting {
userName: string
}


interface INewStuff {
email: string
}


const existingObject: IExisting = {
userName: "jsmith"
}


const objectWithAllProps: IExisting & INewStuff = Object.assign({}, existingObject, {
email: "jsmith@someplace.com"
})


console.log(objectWithAllProps.email); // jsmith@someplace.com

优势

  • 整个过程中的类型安全,因为您根本不需要使用any类型
  • 使用TypeScript的聚合类型(在声明objectWithAllProps的类型时由&表示),它清楚地传达了我们正在动态地(即动态地)组成一个新类型

需要注意的事情

  1. Object.assign有自己独特的方面(大多数有经验的JS开发人员都知道),在编写TypeScript时应该考虑这些方面。
    • 它可以以可变的方式或不可变的方式使用(我在上面演示了不可变的方式,这意味着existingObject保持不变,因此没有email属性。对于大多数函数式程序员来说,这是一件好事,因为结果是唯一的新更改)。
    • 当你有更扁平的对象时,Object.assign效果最好。如果你要组合两个包含可为空属性的嵌套对象,你最终可能会用未定义覆盖true值。如果你注意Object.assign参数的顺序,你应该没问题。

既然你不能这样做:

obj.prop = 'value';

如果您的TS编译器和linter没有严格要求您,您可以这样写:

obj['prop'] = 'value';

如果您的TS编译器或linter是严格的,另一个答案是类型转换:

var obj = {};
obj = obj as unknown as { prop: string };
obj.prop = "value";

最简单的将跟随

const obj = <any>{};
obj.prop1 = "value";
obj.prop2 = "another value"

您可以使用扩展运算符基于旧对象创建新对象

interface MyObject {
prop1: string;
}


const myObj: MyObject = {
prop1: 'foo',
}


const newObj = {
...myObj,
prop2: 'bar',
}


console.log(newObj.prop2); // 'bar'

TypeScript将推断原始对象的所有字段,VSCode将执行自动完成等操作。

你可以使用这个:

this.model = Object.assign(this.model, { newProp: 0 });

试试这个:

export interface QueryParams {
page?: number,
limit?: number,
name?: string,
sort?: string,
direction?: string
}

那就用它

const query = {
name: 'abc'
}
query.page = 1

这是Object.assign的特殊版本,它会随着每次属性更改自动调整变量类型。不需要额外的变量、类型断言、显式类型或对象副本:

function assign<T, U>(target: T, source: U): asserts target is T & U {
Object.assign(target, source)
}


const obj = {};
assign(obj, { prop1: "foo" })
//  const obj now has type { prop1: string; }
obj.prop1 // string
assign(obj, { prop2: 42 })
//  const obj now has type { prop1: string; prop2: number; }
obj.prop2 // number


//  const obj: { prop1: "foo", prop2: 42 }

注意:样本使用TS 3.7断言函数assign的返回类型是void,与Object.assign不同。

唯一完全类型安全的解决方案是这一个,但有点冗长,迫使您创建多个对象。

如果你必须先创建一个空对象,然后从这两个解决方案中选择一个。请记住,每次你使用as,你都在失去安全性。

更安全的解决方案

object的类型是getObject中的安全,这意味着object.a的类型是string | undefined

interface Example {
a: string;
b: number;
}


function getObject() {
const object: Partial<Example> = {};
object.a = 'one';
object.b = 1;
return object as Example;
}

简短的解决方案

object的类型是getObject中的不安全,这意味着object.a甚至在赋值之前就是string类型。

interface Example {
a: string;
b: number;
}


function getObject() {
const object = {} as Example;
object.a = 'one';
object.b = 1;
return object;
}

为Angular扩展@jmvtrinidad解决方案,

当使用已经存在的类型化对象时,这是如何添加新属性。

let user: User = new User();
(user as any).otherProperty = 'hello';
//user did not lose its type here.

现在如果你想在html端使用otherProperty,这是你需要的:

<div *ngIf="$any(user).otherProperty">
...
...
</div>

Angular编译器将$any()视为any类型的转换,就像在TypeScript中使用<any>as any转换时一样。

我在尝试对充当状态存储的对象进行部分更新时遇到了这个问题。

type State = {
foo: string;
bar: string;
baz: string;
};


const newState = { foo: 'abc' };


if (someCondition) {
newState.bar = 'xyz'
}


setState(newState);

在这种情况下,最好的解决方案是使用Partial<T>。它使用?令牌使提供的类型上的所有属性都是可选的。在关于使类型上的所有属性成为可选的更具体的SO主题中阅读有关它的更多信息。

这是我用Partial<T>解决的方法:

type State = {
foo: string;
bar: string;
baz: string;
};


const newState: Partial<State> = { foo: 'abc' };


if (someCondition) {
newState.bar = 'xyz';
}


setState(newState);

这与fregante在他们的回答中描述的类似,但我想为这个特定的用例(在前端应用程序中很常见)描绘一个更清晰的画面。

我写了一篇关于这个主题的文章:

TypeScript-在运行时增强对象及其类型

https://tech.xriba.io/2022/03/24/typescript-enhance-an-object-and-its-type-at-runtime/

也许你可以从TypeScript的概念中获得灵感:

迟了,但答案很简单

let prop = 'name';
let value = 'sampath';
this.obj = {
...this.obj,
[prop]: value
};

只要map可以采用固定类型的真正任意值,则使用ES6Map,否则使用可选属性

我认为这是我要去的指导方针。ES6地图可以用打字稿完成,如打字稿中的ES6地图所述

可选属性的主要用例是函数的“选项”参数:使用命名参数JavaScript(基于打字脚本)在这种情况下,我们确实提前知道允许属性的确切列表,所以最明智的做法是定义一个显式接口,并使任何可选的?可选,如https://stackoverflow.com/a/18444150/895245所述,以获得尽可能多的类型检查:

const assert = require('assert')


interface myfuncOpts {
myInt: number,
myString?: string,
}


function myfunc({
myInt,
myString,
}: myfuncOpts) {
return `${myInt} ${myString}`
}


const opts: myfuncOpts = { myInt: 1 }
if (process.argv.length > 2) {
opts.myString = 'abc'
}


assert.strictEqual(
myfunc(opts),
'1 abc'
)

然后我将使用Map,当它是真正任意的(无限多个可能的键)和固定类型时,例如:

const assert = require('assert')
const integerNames = new Map<number, string>([[1, 'one']])
integerNames.set(2, 'two')
assert.strictEqual(integerNames.get(1), 'one')
assert.strictEqual(integerNames.get(2), 'two')

测试:

  "dependencies": {
"@types/node": "^16.11.13",
"typescript": "^4.5.4"
}