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 likeinterfaces
ortypes
.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
, andboolean
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.