Mental Model For Typescript's Type System

Mental Model For Typescript's Type System

This article is based on my learning from the book Effective Typescript by Dan Vanderkam

Think of TypeScript’s type system as a "domain for JavaScript." It's a layer that adds structure and safety to JavaScript, which is inherently dynamic and loosely typed. Understanding how TypeScript works helps you write safer, more predictable code. Let's break down some key concepts that will help you form a solid mental model for TypeScript.

1. Types vs. Values in TypeScript

A common source of confusion is the difference between the type space and the value space in TypeScript:

  • Type Space: This is where TypeScript defines types like number, string, boolean, Array<T>, and user-defined types like interfaces or types.

  • Value Space: This is the actual JavaScript runtime where values like 5, 'hello', or ['item1', 'item2'] exist.

Understanding this separation is crucial because TypeScript types exist only during development (at compile-time) and are erased in the compiled JavaScript output.

2. Interface vs. Type

While both interface and type can be used to define object shapes, there are subtle differences:

  • Interface: Best used when you need to define the shape of an object and can extend other interfaces. It's more suited for object-oriented programming models.

      typescriptCopy codeinterface Person {
        name: string;
        age: number;
      }
    
  • Type: More flexible and can be used to define unions, intersections, tuples, or primitives. Use it for more complex or conditional types.

      typescriptCopy codetype Point = { x: number; y: number };
      type StringOrNumber = string | number;
    

3. Type Declarations vs. Type Assertions

  • Type Declarations: You declare the type of a variable or function parameter. Type declarations enable TypeScript's type-checking capabilities and help catch errors early.

      typescriptCopy codeconst age: number = 25;
    
  • Type Assertions: Use when you know more about a value than TypeScript does, and you want to override TypeScript's type inference. Use assertions sparingly, as they can lead to unsafe code.

      typescriptCopy codeconst input = document.getElementById('username') as HTMLInputElement;
      input.value = 'TypeScript';
    

4. Type Checking and Primitives

JavaScript has several primitive types (like string, number, boolean) and their object wrappers (String, Number, Boolean). TypeScript recognizes both but focuses on the primitive versions for type checking.

  • TypeScript Primitives: Types like string, number, and boolean are key to defining most TypeScript code.

5. Type Checking Does Not Happen at Runtime

TypeScript is a static type checker; it does not enforce types at runtime. If you want to ensure runtime safety, you need to write runtime checks yourself.

6. Type Assertions and Type Declarations

Prefer type declarations over type assertions because declarations provide TypeScript with more information, helping it catch more errors. For example, this code avoids a common mistake:

typescriptCopy codeconst user = { name: "Alice", age: 25 }; // inferred type

versus:

typescriptCopy codeconst user = { name: "Alice", age: 25 } as const; // type assertion

7. The "Any" Type

Using any is like telling TypeScript to "turn off" type checking for a particular variable. It should be avoided wherever possible because it removes all the benefits TypeScript provides.

8. Type Inference

TypeScript can often infer the type of a variable or function return type:

typescriptCopy codeconst x = 12; // TypeScript infers that x is of type number

However, type inference does not work well for function parameters:

typescriptCopy codefunction greet(name: string) {
  return `Hello, ${name}`;
}

Explicitly typing parameters is necessary for proper type safety.

9. Excess Property Checking

By specifying types for objects, TypeScript checks if there are extra or missing properties, helping catch bugs:

typescriptCopy codeconst user = { name: "Alice", age: 30 };
const person: { name: string; age: number } = user; // OK

10. Widening and Narrowing

TypeScript uses widening and narrowing to infer more general or specific types.

  • Widening: When TypeScript expands a specific type to a more general one.

      typescriptCopy codeconst x = 1; // Type is inferred as number (widened)
    
  • Narrowing: When TypeScript reduces a broader type to a more specific one, based on control flow.

      typescriptCopy codeconst el = document.getElementById('foo');
      if (el) {
        el.innerHTML = 'Hello'; // Type is narrowed to HTMLElement
      }
    

11. Type Guards

You can define custom functions to help TypeScript narrow down types in complex scenarios:

typescriptCopy codefunction isString(value: any): value is string {
  return typeof value === 'string';
}

12. Contextual Typing

Avoid separating a value from its context. For example:

typescriptCopy codefunction panTo(where: [number, number]) {}
panTo([10, 20]); // OK

const loc = [10, 20]; // Bad, as the type context is lost

Better:

typescriptCopy codeconst loc: [number, number] = [10, 20];

13. Type Design Principles

  • Be Liberal in What You Accept and Strict in What You Produce: Accept a broad range of inputs and produce specific outputs.

  • Use the Narrowest Possible Scope for Types: The narrower your types, the more specific your functions and objects can be.

14. Version Management for Type Declarations

Understand that every package can have three versions to consider:

  • The version of the package.

  • The version of its type declarations (e.g., @types package).

  • The version of TypeScript itself.

15. Use TSDoc for API Comments

Document your code using TSDoc, the TypeScript standard for inline documentation.

typescriptCopy code/**
 * Generate a greeting.
 * @param name Name of the person to greet
 * @param title The person's title
 * @returns A greeting formatted for human consumption.
 */
function greetFullTSDoc(name: string, title: string) {
  return `Hello ${title} ${name}`;
}

16. Provide a Type for this in Callbacks

Ensure you understand how this works and provide types for it in callbacks, especially when it's part of your API.

17. DOM Types

Know the differences between Node, Element, HTMLElement, and EventTarget. Use the most specific type possible to prevent errors.

18. Debugging TypeScript

Use source maps to debug your TypeScript code, ensuring you can trace errors back to your TypeScript code rather than the compiled JavaScript.

Did you find this article valuable?

Support Pratik Sharma by becoming a sponsor. Any amount is appreciated!