Execute Program

Everyday TypeScript: Object Values at Runtime

Welcome to the Object Values at Runtime lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We've seen that TypeScript will let us narrow object types.

  • >
    const user = {name: 'Amir', email: 'amir@example.com'};
    const hasEmail: {email: string} = user;
    hasEmail.email;
    Result:
    'amir@example.com'Pass Icon
  • There's something very strange here, but it's not immediately obvious. When we compile TypeScript, we get JavaScript out. The TypeScript language was designed to make that transformation easy, so in most cases it only requires removing the type information. The compiler will compile our code above into approximately this JavaScript code:

  • >
    const user = {name: 'Amir', email: 'amir@example.com'};
    const hasEmail = user;
    hasEmail.email;
  • What value does hasEmail have? We never did anything to remove the name property, so hasEmail still has a name! Of course, TypeScript won't let us access it directly because it's not part of hasEmail's type.

  • >
    const user = {name: 'Amir', email: 'amir@example.com'};
    const hasEmail: {email: string} = user;
    hasEmail.name;
    Result:
    type error: Property 'name' does not exist on type '{ email: string; }'.Pass Icon
  • Despite the type error, the name property is still there at runtime! We can access it by using as any to defeat the type system.

  • >
    const user = {name: 'Amir', email: 'amir@example.com'};
    const hasEmail: {email: string} = user;
    (hasEmail as any).name;
    Result:
    'Amir'Pass Icon
  • This may seem like an unimportant detail. If the type system doesn't normally let us access the extra property, does it really matter?

  • Unfortunately, it matters a lot! If we look at the entire hasEmail object, it will have both name and email. That can be confusing during debugging: if we print an object, for example with console.log(...), it might have many unexpected properties.

  • >
    const user = {name: 'Amir', email: 'amir@example.com'};
    const hasEmail: {email: string} = user;
    hasEmail;
    Result:
    {name: 'Amir', email: 'amir@example.com'}Pass Icon
  • Things get worse when we serialize our object into JSON. For example, we might use the built-in JSON.stringify function. But JSON.stringify is part of JavaScript, not TypeScript, so there's no way for it to know which properties it should serialize. It always serializes all properties, even properties that aren't in the type.

  • >
    const user = {name: 'Amir', email: 'amir@example.com'};
    const hasEmail: {email: string} = user;
    JSON.stringify(hasEmail);
    Result:
  • When we deserialize the JSON later, on the other side of a network connection for example, it will have both a name and email property.

  • >
    const user = {name: 'Amir', email: 'amir@example.com'};
    const hasEmail: {email: string} = user;
    const stringified = JSON.stringify(hasEmail);
    JSON.parse(stringified);
    Result:
    {name: 'Amir', email: 'amir@example.com'}Pass Icon
  • This can cause serious real-world problems. For example, we might have a user object with many megabytes of data in its properties. Then we narrow its type down to just a couple of properties, like {id: 1234, name: 'Amir'}, and finally convert it into JSON to send it to an API. But when we call JSON.stringify on that object, it will still have those megabytes of unnecessary properties, causing a serious performance problem!

  • This can also cause security problems. Our server-side database records might include sensitive data, like credit card information or personal contact information. If we narrow our records like we did for hasEmail above, it may look like those sensitive properties are gone. But at runtime they're still there, so our API server will return them to the client! Imagine viewing a user's profile on a site like GitHub, using your browser's developer tools to inspect the API payloads, and finding that user's credit card number!

  • There are many ways to work around this problem, but your choices will depend on your technology stack and your team. However, it's important to keep the central idea of this lesson in mind: the TypeScript compiler checks the types, then throws them away and emits JavaScript code. TypeScript types can't affect runtime data.