What I Learned at Work this Week: TypeScript “Property does not exist on type”
Something funny happened to me last week. I was helping a fellow engineer study for an interview and when they started writing a loop, I thought of Python syntax before JavaScript. This might not sound so unusual, but for a JS native like me, it was a big shift. I have struggled with Python for a long time and expressed my frustration that it wasn’t as “readable” as its reputation promised. But because of time spent immersing myself in the language, it had started to become natural to me and I better understood why it was so popular.
That feeling of excitement was mixed with a little bit of disappointment. I really like JavaScript and enjoyed learning more about TypeScript as well. As engineers, we’re always faced with the question of whether we’re spending our time on something that’s going to help us grow, something that will advance our careers or make us better teammates (actually I bet this affects people in every industry). I wondered whether I had focused too much on expanding the breadth of my knowledge rather than the depth when it came to JS.
Ultimately, these feelings are fleeting because at my core I believe that any learning and practice is a step in the right direction and I’m not on a strict timeline to “master” the concepts I study. And the experience was helpful because it reminded me that I am interested in TypeScript and should seek out opportunities to practice it. If you’ve read the title of this post, you know that I found one this past week.
Adding TypeScript
I first wrote about TypeScript when one of the main tools at my workplace was rebuilt using the language. And so it should come as no surprise that TypeScript is back on my radar because yet another library of ours is getting a fresh refactor, complete with a TS upgrade. This time, it’s a series of modules that contain client-specific logic. For example, a client may want to run custom JS when we trigger a pop up asking a user to sign up for our service. To account for these requests, we have a condition in our code that says “if the current client has a custom logic module with a function associated with the event currently being fired, trigger that function as well.”
To clear this up a little, here’s a simplified sample of what a module looked like before TypeScript:
// diazcoCustomLogic.jsconst diazcoConfig = {
SIGNUP_EVENT: () => {
window.dataLayer.push({eventType: 'signup'})
}
};export default diazcoConfig;
So if our code is running for Diaz Co., we’ll push this new object to our dataLayer on a signup event. This might be difficult to fully grasp without domain knowledge of the product, but the important part is that we’ve created an object that contains key-value pairs where the key is an event type and the value is a function to be executed on the event type.
There are finite number of event types that we can use as triggers, so this is a great opportunity to create an interface. Defining the possible options for keys in our exported object will save us from defining a function that will never ben invoked. And so the code was updated for each of these modules to look more like this:
import { CustomClientConfig } from '../custom/getClientConfig';
const diazcoConfig: CustomClientConfig = {
// keys and values of our CustomClientConfig
}
CustomClientConfig is imported from its own file, where we list about 50 possible keys and the data types of their associated values (some, like my example, are functions, but others can be objects, booleans, etc). None of these caused me any issue, but when I actually tried to put custom logic into a function, I got a lot of errors, the peskiest of which was…
TS2339: Property does not exist on type
Let’s look at the code that threw this error:
const diazcoConfig: CustomClientConfig = {
SIGNUP_EVENT: data => {
try {
document.getElementById("form-123")
.addEventListener("click", function(){
const uEmail =
document.getElementById('input-456').value;
window._rsq.push(['_setUserEmail', uEmail]);
});
} catch (err) {
//handle error
}
},
};
Once again, this is simplified, but check out the bolded part, which gave me an error. The full text of this error, in this specific case, is:
TS2339: Property '_rsq' does not exist on type 'Window & typeof globalThis'.
Before we go any further, note that this error has an ID number (TS2339). Take note of that since it makes it easier to search for fixes.
Now if you’re not sure what _rsq is, you’re right in the same boat as TypeScript. This isn’t a default property of the Window class, but is actually a custom array used for ecommerce sites built using Shopify. We don’t have to know exactly how it works, but we can infer that it’s likely added as a property of the window by the client’s Shopify integration, so we can expect it to be available when this code attempts to push a new element into it. And even if we’re wrong, the function is inside of a try/catch, so the error will be handled.
But TypeScript isn’t comfortable with that assumption, so it throws this error. To TS, Window is a type that has a series of available properties. _rsq isn’t one of them, so this code is bound to fail. To give it any chance of success, we have to update the definition of the window interface. I didn’t realize it when I first ran into this issue, but that was already being done in that same file, which redefines window:
interface customWindow extends Window {
}
declare const window: customWindow;
The first line here creates an interface that extends Window and adds nothing to it. It’s basically a copy of the default Window interface. The declaration states that whenever we reference window in our code, it’ll be of the type we’ve just defined. So if we want to tell TypeScript that window._rsq will be valid, we can add that to the interface!
interface customWindow extends Window {
_rsq: Array<any>
}
declare const window: customWindow;
This gets rid of the error, but we still have to be careful. In this case, we’re saying that window will always have an _rsq attribute of a certain type, which isn’t something we can promise since the page itself is built by our client. We’re also making an assumption that _rsq is an Array, but for all we know it’s a custom object that has a push method that does something totally different than what we read in the linked Retention Science article. Though it’s very vague, this is the code I ended up using:
interface customWindow extends Window {
_rsq?: any
}
declare const window: customWindow;
This doesn’t really give our code any useful information. It says that there may or may not be an _rsq attribute in the window, and that this attribute could be of any data type under the sun. We’ve more-or-less added it to get rid of the error, but this is a fact of life when you’re working with Software as a Service. You’re never going to be fully aware of the environment where your code is implemented and you often have to account for a variety of different possibilities. Even if this code is only implemented by one client, we don’t want it to break or throw an error if the client changes their site without telling us.
Strict typing can sometimes make us jump through hoops just to satisfy a linter, but it’s there for our protection. Even this code, which doesn’t give us a lot of information, lets us know that a new property is being injected into the window. More importantly, refactoring our code in TypeScript makes us more confident that what we write matches the logic we’ve defined elsewhere and makes our work easier to test in general. And, of course, it can help us study something we have previously forgotten.
Sources
- Shopify email capture setup (Required), Grace Malloy, Retention Science
- error TS2339: Property ‘x’ does not exist on type ‘Y’, Stack Overflow