Editor’s note: This article was updated on 6 October 2023, introducing solutions like type assertions and the Partial
utility type to address the TypeScript error highlighted.
JavaScript is a dynamically typed language, meaning that a variable’s type is determined at runtime and by what it holds at the time of execution. This makes it flexible but also unreliable and prone to errors because a variable’s value might be unexpected.
TypeScript, on the other hand, is a statically typed version of JavaScript — unlike JavaScript, where a variable can change types randomly, TypeScript defines the type of a variable at its declaration or initialization.
Dynamic property assignment is the ability to add properties to an object only when they are needed. This can occur when an object has certain properties set in different parts of our code that are often conditional.
In this article, we will explore some ways to enjoy the dynamic benefits of JavaScript alongside the security of TypeScript’s typing in dynamic property assignment.
Consider the following example of TypeScript code:
const organization = {} organization.name = "Logrocket"
This seemingly harmless piece of code throws a TypeScript error when dynamically assigning name
to the organization
object:
See this example in the TypeScript Playground.
The source of confusion, perhaps rightly justified if you’re a TypeScript beginner, is: how could something that seems so simple be such a problem in TypeScript?
The TL;DR of it all is that if you can’t define the variable type at declaration time, you can use the Record
utility type or an object index signature to solve this. But in this article, we’ll go through the problem itself and work toward a solution that should work in most cases.
Generally speaking, TypeScript determines the type of a variable when it is declared. This determined type stays the same throughout your application. There are exceptions to this rule, such as when considering type narrowing or working with the any
type, but otherwise, this is a general rule to remember.
In the earlier example, the organization
object is declared as follows:
const organization = {}
There is no explicit type assigned to this variable, so TypeScript infers a type of organization based on the declaration to be {}
, i.e., the literal empty object.
If you add a type alias, you can explore the type of organization
:
type Org = typeof organization
See this in the TypeScript Playground.
When you then try to reference the name
prop on this empty object literal:
organization.name = ...
You receive the following error:
Property 'name' does not exist on type ‘ {}‘
There are many ways to solve the TypeScript error here. Let’s consider the following:
This is the easiest solution to reason through. At the time you declare the object, go ahead and type it, and assign all the relevant values:
type Org = { name: string } const organization: Org = { name: "Logrocket" }
See this in the TypeScript Playground.
This eliminates any surprises. You’re clearly stating what this object type is and rightly declaring all relevant properties when you create the object.
However, this is not always feasible if the object properties must be added dynamically, which is why we’re here.
Occasionally, the properties of the object truly need to be added at a time after they’ve been declared. In this case, you can use the object index signature, as follows:
type Org = {[key: string] : string} const organization: Org = {} organization.name = "Logrocket"
See this in the TypeScript Playground.
When the organization
variable is declared, you can explicitly type it to the following: {[key: string] : string}
.
You might be used to object types having fixed property types:
type obj = { name: string }
However, you can also substitute name
for a “variable type.” For example, if you want to define any string property on obj
:
type obj = { [key: string]: string }
Note that the syntax is similar to how you’d use a variable object property in standard JavaScript:
const variable = "name" const obj = { [variable]: "Freecodecamp" }
The TypeScript equivalent is called an object index signature.
Moreover, note that you could type key
with other primitives:
// number type Org = {[key: number] : string} // string type Org = {[key: string] : string} //boolean type Org = {[key: boolean] : string}
Record
Utility TypeThe Record
utility type allows you to constrict an object type whose properties are Keys
and property values are Type
. It has the following signature: Record<Keys, Type>
.
In our example, Keys
represents string
and Type
. The solution here is shown below:
type Org = Record<string, string> const organization: Org = {} organization.name = "Logrocket"
Instead of using a type alias, you can also inline the type:
const organization: Record<string, string> = {}
See this in the TypeScript Playground.
Map
data typeA Map
object is a fundamentally different data structure from an object
, but for completeness, you could eliminate this problem if you were using Map
.
Consider the starting example rewritten to use a Map
object:
// before const organization = {} organization.name = "Logrocket" // after const organization = new Map() organization.set("name","Logrocket")
With Map
objects, you’ll have no errors when dynamically assigning properties to the object:
See this in the TypeScript Playground.
This seems like a great solution at first, but the caveat is your Map
object is weakly typed. You can access a nonexisting property and get no warnings at all:
const organization = new Map() organization.set("name","Logrocket") // Property nothingness does not exist. No TS warnings const s = organization.get("nothingness") console.log(s)
See the TypeScript Playground.
This is unlike the standard object. By default, the initialized Map
has the key and value types as any
— i.e., new () => Map<any, any>
. Consequently, the return type of the s
variable will be any
:
When using Map
, at the very least, I strongly suggest passing some type information upon creation. For example:
const organization = new Map<string, string>() organization.set("name","Logrocket") const s = organization.get("nothingness") console.log(s)
s
will still be undefined, but you won’t be surprised by its code usage. You’ll now receive the appropriate type for it:
If you truly don’t know what the keys
of the Map
will be, you can go ahead and represent this at the type level:
const organization = new Map<unknown, string>()
And if you’re not sure what the keys
or values
are, be safe by representing this at the type level:
const organization = new Map<unknown, unknown>()
object
propertyThis solution won’t always be possible, but if you know the name of the property to be dynamically assigned, you can optionally provide this when initializing the object as shown below:
const organization : {name?: string} = {} organization.name = "Logrocket"
See the TypeScript Playground.
If you don’t like the idea of using optional properties, you can be more explicit with your typing as shown below:
const organization : {name: string | null} = { name: null } organization.name = "Logrocket"
See the TypeScript Playground.
TypeScript type assertion is a mechanism that tells the compiler the variable’s type and overrides what it infers from the declaration or assignment. With this, we are telling the compiler to trust our understanding of the type because there will be no type verification.
We can perform a type assertion by either using the <>
brackets or the as
keyword. This is particularly helpful with the dynamic property assignment because it allows the properties we want for our object to be dynamically set because TypeScript won’t enforce them.
Let’s take a look at applying type assertions to our problem case:
interface Org { name: string } // using <> const organization = <Org> {} organization.name = "Logrocket" // or using as const otherOrganization = {} as Org otherOrganization.name = "not Logrocket"
See the TypeScript Playground.
Note that with type assertions, the compiler is trusting that we will enforce the type we have asserted. This means if we don’t, for example, set a value for
organization.name
, it will throw an error at runtime that we will have to handle ourselves.
Partial
utility typeTypeScript provides several utility types that can be used to manipulate types. Some of these utility types are Partial
, Omit
, Required
, and Pick
.
For dynamic property assignments, we will focus specifically on the Partial
utility type. This takes a defined type and makes all its properties optional. Thus, we can initialize our object with any combination of its properties, from none to all, as each one is optional:
interface Org { name: string phoneNumber: string } const organization: Partial<Org> = {} organization.name = "Logrocket"
In our example with the Partial
utility type, we defined our organization object as the type partial Org
, which means we can choose not to set a phoneNumber
property:
See the TypeScript Playground.
In this article, we explored the different options for setting properties dynamically in TypeScript. These options can be grouped together by their similarities.
This group of options allows you to define the type of keys allowed without limiting what possible keys can exist. The options in this group include:
Record
utility typeMap
data type (with key/value typing)With these, we can define that our object will take string indexes and decide what types to support as values, like String
, Number
, Boolean
, or Any
:
// Using an Object Index Signature const object1: {[key: string]: string} = {} object1.name = "Tammy" // Using the Record Utility Type const object2: Record<string, number> = {} object2.count = 10 // Using the Map data type const object3 = new Map<string, any>() object3.set("name", "Tammy") object3.set("count", 10) object3.set("isFull", false)
Pro: The main benefit of these methods is the ability to dynamically add properties to an object while still setting expectations for the potential types of keys and values.
Con: The main disadvantage of this way of defining objects is that you can’t predict what keys our objects will have and so some references may or may not be defined. An additional disadvantage is that if we decide to define our key signature with type Any
, then the object becomes even more unpredictable.
This set of object assignment methods shares a common feature: the definition of optional properties. This means that the range of possible properties are known but some may or may not be set. The options in this group include:
Partial
utility typeSee this example in the TypeScript Playground, or in the code block below:
// Optional object properties interface Obj { name?: string size?: number } interface FullObj { name: string size: number } const object1: Obj = {} object1.name = "Rocket" // Partial Utility Type const object2: Partial<FullObj> = {} object2.size = 10 // Type Assertion const object3 = <FullObj> {} object3.name = "New Rocket"
Note: While these options mean that the possible keys are known and may not be set, TypeScript’s compiler won’t validate undefined states when using type assertions. This can lead to unhandled exceptions during runtime. For example, with optional properties and the
Partial
utility type,name
has typestring
orundefined
. Meanwhile, with type assertions,name
has typestring
.
Pro: The advantage of this group of options is that all possible object keys and values are known.
Con: The disadvantage is that while the possible keys are known, we don’t know if those keys have been set and will have to handle the possibility that they are undefined.
Apart from primitives, the most common types you’ll have to deal with are likely object types. In cases where you need to build an object dynamically, take advantage of the Record
utility type or use the object index signature to define the allowed properties on the object.
If you’d like to read more on this subject, feel free to check out my cheatsheet on the seven most-asked TypeScript questions on Stack Overflow, or tweet me any questions. Cheers!
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowAstro, renowned for its developer-friendly experience and focus on performance, has recently released a new version, 4.10. This version introduces […]
In web development projects, developers typically create user interface elements with standard DOM elements. Sometimes, web developers need to create […]
Toast notifications are messages that appear on the screen to provide feedback to users. When users interact with the user […]
Deno’s features and built-in TypeScript support make it appealing for developers seeking a secure and streamlined development experience.
3 Replies to "How to dynamically assign properties to an object in TypeScript"
I know this is explicitly for TypeScript, and I think type declarations should always be first. But in general, you can also use a Map object. If it’s really meant to by dynamic, might as well utilize the power of Map.
Great suggestion (updated the article). It’s worth mentioning the weak typing you get by default i.e., with respect to Typescript.
Hi, thanks for your valuable article
please consider ‘keyof type’ in TypeScript, and add this useful solution if you are happy
have nice time