Last updated: Feb 27, 2024
Reading timeยท10 min
To add a property to an object in TypeScript:
interface Person { name: string; age?: number; // ๐๏ธ mark as optional so you can add it later } const obj: Person = { name: 'Bobby Hadz', }; obj.age = 30;
We set the age
property on the Person
interface to
optional.
age
property can either be undefined
or a number
. This approach is used when you know the name of the property and the type of its value ahead of time as it keeps things type-safe.Now you can initialize the object without the property and set it later on.
interface Person { name: string; age?: number; // ๐๏ธ mark as optional } const obj: Person = { name: 'Bobby Hadz', }; obj.age = 30; // โ Error: Type 'string' is not assignable to type 'number'. obj.age = 'hello';
If you try to assign a non-numeric value to the age
property, you'd get an
error.
If you need to make all properties of an object optional, use the Partial utility type.
You can use the Record utility type to add any property of any type to an object.
const obj: Record<string, any> = {}; // ๐๏ธ can now add any property to the object obj.name = 'Bobby Hadz'; obj.age = 30; // ๐๏ธ { name: 'Bobby Hadz', age: 30 } console.log(obj);
The Record
utility type allows us to enforce the type of an object's values in
TypeScript, e.g. type Animal = Record<string, string>
.
Record
utility type constructs an object type, whose keys and values are of the specified type.We used a type of any
for the values and a type of string
for the keys in
the object.
This is very broad and allows us to add any property of any
type to the
object.
You can specify the properties and the types in an object that you know about
and use the Record
utility type to allow the user to add other properties.
interface Animal extends Record<string, any> { name: string; age: number; } const obj: Animal = { name: 'Alfred', age: 3 }; // ๐๏ธ Can now add any property, but name and age are typed obj.type = 'Dog';
The interface we created requires
the name
and age
properties but also extends a type that allows any string
properties with values of any
type.
This is better than just using the Record
utility type with string
keys and
any
values because we get type safety when it comes to the properties we
explicitly specified.
interface Animal extends Record<string, any> { name: string; age: number; } const obj: Animal = { name: 'Alfred', age: 3 }; // โ๏ธ Type 'number' is not assignable to type 'string'. obj.name = 5;
Setting a property to the incorrect type causes an error because we've already
declared the name
property to be of type string
in the Animal
interface.
I've also written an article on how to get an object's key by value in TS.
Use an index signature to dynamically add properties to an object.
Index signatures are used when we don't know all of the names of a type's properties and the type of their values ahead of time.
interface Person { [key: string]: any; } const obj: Person = {}; obj.name = 'Bobby Hadz'; obj.age = 30;
The {[key: string]: any}
syntax is an
index signature in TypeScript and is used
when we don't know all the names of a type's properties and the shape of the
values ahead of time.
string
, it will return a value of any
type.You might also see the index signature {[key: string]: string}
in examples. It
represents a key-value structure that when indexed with a string
returns a
value of type string
.
I've also written an article on how to dynamically access an object's property.
If the value of the string keys were set to any
in the index signature, you
could add properties to the object of any type, since anything is more specific
than any
.
interface Person { [key: string]: any; age: number; name: string; country?: string; } const obj: Person = { name: 'Bobby Hadz', age: 30, }; obj.country = 'Chile'; obj.language = 'TypeScript';
This is a good way to narrow down the type of some of the properties that you know ahead of time.
For example, if I try to set the age
property to a string
, the type checker
would throw an error because it expects a number
.
interface Person { [key: string]: any; age: number; name: string; country?: string; } const obj: Person = { name: 'Bobby Hadz', age: 30, }; // โ๏ธ Error: Type 'string' is not assignable to type 'number'. obj.age = '100';
We've set the age
property to have a type of number
, so the type checker
helps us spot an error in our application.
You can also use a union type to dynamically add properties to an object.
interface Person { // ๐๏ธ key value [key: string]: string | number; } const obj: Person = { name: 'Bobby Hadz', }; obj.age = 30; obj.country = 'Chile';
string
, it will return a value of typestring
or number
.The keys in the object can either have a type of string
or number
.
Both types are allowed and you can use the union |
syntax to include as many
types as necessary.
If we try to set a property with a different value on the object, we'd get an error.
interface Person { // ๐๏ธ key value [key: string]: string | number; } const obj: Person = { name: 'Bobby Hadz', }; // โ๏ธ Type 'boolean' is not assignable to type 'string | number'.ts(2322) obj.isProgrammer = true;
Record
typeThe Record
utility type constructs an object type whose keys and values are of
a specified type.
const obj: Record<string, any> = { name: 'Bobby Hadz', }; obj.age = 30; obj.country = 'Chile';
We typed the object above to have keys of type string
and values of type
any
.
Keys
, and the second is the type for the values.If you know the type of some of the values ahead of time, specify them in an interface for better type safety.
interface EmployeeData extends Record<string, any> { role: string; salary: number; color?: string; } const employees: Record<string, EmployeeData> = { tom: { role: 'accountant', salary: 10, color: 'blue' }, alice: { role: 'manager', salary: 15 }, }; // โ๏ธ Type 'number' is not assignable to type 'string'.ts(2322) employees.tom.role = 100; // โ still works employees.tom.hello = 'world';
The interface EmployeeData
extends from the Record
constructed type with
string keys and any
type values.
3
properties that we know about in advance.If we try to set the role
, salary
or color
properties to an incompatible
type, we'd get an error.
Use computed property names to set an object's property name from a variable in TypeScript.
type Person = { name: string; country: string; }; const myVar = 'name'; const obj: Person = { [myVar]: 'Bobby Hadz', country: 'Chile', }; // ๐๏ธ { name: 'Bobby Hadz', country: 'Chile' } console.log(obj);
We set an object's property name from a variable using computed properties.
[]
for it to get evaluated before being assigned as a property on the object.Here is an example that uses a function.
type Person = { name: string; country: string; }; const myVar = 'name'; function getObj(): Person { return { [myVar]: 'Bobby Hadz', country: 'Chile', }; } const obj = getObj(); // ๐๏ธ {name: 'Bobby Hadz', country: 'Chile'} console.log(obj);
The expression between the square brackets gets evaluated, so you could construct the object's property name by using multiple variables or concatenating strings.
const myVar = 'na'; const myVar2 = 'me'; // ๐๏ธ const obj: { [x: string]: string; country: string;} const obj = { [myVar + myVar2]: 'Bobby Hadz', country: 'Chile', }; // ๐ {name: 'Bobby Hadz', country: 'Chile'} console.log(obj);
You can also use a template literal.
const myVar = 'na'; const myVar2 = 'me'; // ๐๏ธ const obj: { [x: string]: string; country: string;} const obj = { [`${myVar}${myVar2}`]: 'Bobby Hadz', country: 'Chile', }; // ๐ {name: 'Bobby Hadz', country: 'Chile'} console.log(obj);
However, notice that TypeScript was not able to type the property in the object
as name
and instead typed it as a more generic index signature of
[x: string]
(any string property).
If you are sure about the value the expression will evaluate to, use a type assertion.
const myVar = 'na'; const myVar2 = 'me'; // ๐๏ธ const obj: const obj: { name: string; country: string;} const obj = { [(myVar + myVar2) as 'name']: 'Bobby Hadz', country: 'Chile', }; // ๐๏ธ {name: 'Bobby Hadz', country: 'Chile'} console.log(obj);
Now the object is typed correctly and we can still use the dynamic nature of the computed property names feature.
To use the Object.assign()
method in TypeScript, pass a target object as the
first parameter to the method and one or more source objects.
The method will copy the properties from the source objects to the target object.
const obj1 = { name: 'Bobby Hadz' }; const obj2 = { country: 'Chile' }; // ๐๏ธ const result: { name: string; } & { country: string; } const result = Object.assign({}, obj1, obj2); // ๐๏ธ { name: 'Bobby Hadz', country: 'Chile' } console.log(result);
We used the Object.assign method to merge two source objects into a target object.
target
object to which the properties of the source objects are applied.The next parameters are source
objects - objects containing the properties you
want to apply to the target
.
target
In the example, we passed an
empty object as the target
because
you should generally avoid mutating objects as it introduces confusion.
For example, we could've passed obj1
as the target and obj2
as the source.
const obj1 = { name: 'Bobby Hadz' }; const obj2 = { country: 'Chile' }; // ๐๏ธ const result: { name: string; } & { country: string; } const result = Object.assign(obj1, obj2); // ๐๏ธ {name: 'Bobby Hadz', country: 'Chile'} console.log(result); // ๐๏ธ {name: 'Bobby Hadz', country: 'Chile'} console.log(obj1); // โ๏ธ Error: Property 'country' does not // exist on type '{ name: string; }'.ts(2339) console.log(obj1.country);
Note that the target
object is changed in place.
This is especially problematic when using TypeScript because obj1
was changed
in place but its type is still {name: string}
, even though the object contains
a country
property as well.
Object.assign
method.TypeScript uses an intersection type to type the
return value of the Object.assign()
method.
const obj1 = { name: 'Bobby Hadz' }; const obj2 = { country: 'Chile' }; // ๐๏ธ const result: { name: string; } & { country: string; } const result = Object.assign({}, obj1, obj2); // ๐๏ธ { name: 'Bobby Hadz', country: 'Chile' } console.log(result);
In other words, the return value has a type that has all of the members of all
of the objects you passed to the Object.assign()
method.
When you use the Object.assign()
method with objects that have the same
properties, the properties are overwritten by objects that come later in the
parameter list.
const obj1 = { name: 'Bobby Hadz' }; const obj2 = { name: 'James' }; // ๐๏ธ const result: { name: string; } & { name: string; } const result = Object.assign({}, obj1, obj2); console.log(result); // ๐๏ธ {name: 'James'} console.log(result.name); // ๐๏ธ "James"
Both of the objects in the example have a name
property, so the object that is
passed later in the parameters order wins.
I've also written an article on how to initialize a typed empty object in TypeScript.
Object.assign
You should also consider using the
spread syntax (...) as a replacement for
Object.assign()
.
const obj1 = { name: 'Bobby Hadz' }; const obj2 = { country: 'Chile' }; // ๐๏ธ const result: {country: string;name: string;} const result = { ...obj1, ...obj2 }; // ๐๏ธ { name: 'Bobby Hadz', country: 'Chile' } console.log(result);
The spread syntax (...) unpacks the properties of the objects into a new object.
Object.assign()
method.This is generally a better approach because you can't shoot yourself in the foot
by forgetting to provide an empty object as the first parameter of the
Object.assign()
method.
You can unpack as many objects as necessary into a new object and if two objects have the same property, the object that comes later wins.
const obj1 = { name: 'Bobby Hadz' }; const obj2 = { name: 'James' }; // ๐๏ธ const result: {name: string;} const result = { ...obj1, ...obj2 }; console.log(result); // ๐๏ธ {name: 'James'}
Both of the objects have a name
property, so the latter object wins.
You can learn more about the related topics by checking out the following tutorials: