DEV Community

Cover image for Tricksters of Typescript
Muhammadamin
Muhammadamin

Posted on

Tricksters of Typescript

TypeScript is a statically typed superset of JavaScript that helps developers write safer and more maintainable code. It enforces strict typing rules that catch many common programming errors at compile-time rather than runtime. However, like any language, TypeScript has its fair share of quirks and tricks that can catch even experienced developers off guard. In this blog, we'll explore some of the tricksters of TypeScript and provide code examples to help you understand and overcome these challenges.


The "this" Keyword Trickster

One of the common sources of confusion in TypeScript is the behavior of the this keyword. Understanding how this works in different contexts is essential for writing reliable TypeScript code.

class Trickster {
  private name: string = "The Trickster";

  printName() {
    console.log(this.name);
  }

  withTimeout() {
    setTimeout(function () {
      console.log(this.name); // Oops, this.name is undefined!
    }, 1000);
  }
}

const tr = new Trickster();
tr.printName(); // Works as expected
tr.withTimeout(); // Throws an error

Enter fullscreen mode Exit fullscreen mode

In the code above, when you call tr.printName() it logs "The Trickster" to the console as expected. However, when you call tr.withTimeout(), you might expect it to log the same value, but it actually logs undefined. This is because this inside the setTimeout callback refers to a different context.

To fix this, you can use an arrow function, which preserves the outer this context:

withTimeoutFixed() {
  setTimeout(() => {
    console.log(this.name); // Works as expected
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode

The "any" Type Trickster

TypeScript is all about static typing, but sometimes, you might encounter the any type, which effectively turns off type checking. While any can be useful in certain situations, overusing it can undermine the benefits of TypeScript.

function add(a: any, b: any): any {
  return a + b; // No type checking here
}

const result = add(5, "10"); // No error at compile-time
console.log(result); // "510"
Enter fullscreen mode Exit fullscreen mode

Here, the function add accepts two arguments of type any, and it returns any. This means that you can pass any types to this function without TypeScript raising any warnings or errors. In the example, adding a number and a string results in a concatenated string, which might not be what you intended.

To fix this, you should specify the types explicitly or use union types:

function addTyped(a: number, b: number): number {
  return a + b; // Type-safe
}

const result = addTyped(5, 10); // Type error if you pass incompatible types
console.log(result); // 15
Enter fullscreen mode Exit fullscreen mode

The "Never" Type Trickster

The never type in TypeScript represents values that never occur. It is often used in functions that throw exceptions or never return.

function throwError(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {
    // Infinite loop
  }
}
Enter fullscreen mode Exit fullscreen mode

The never type can be tricky to understand, especially when you encounter it for the first time. It essentially signifies that a function won't produce a value, and it's useful for making your code more expressive and error-resistant.


The "Type Assertion" Trickster

Type assertions, denoted by the <Type> or as Type syntax, allow you to tell the TypeScript compiler to treat an expression as a specific type.

const someValue: any = "Hello, TypeScript!";
const strLength: number = (someValue as string).length;
Enter fullscreen mode Exit fullscreen mode

While type assertions can be helpful in certain situations, they should be used cautiously. Relying too much on them can lead to runtime errors and bypass TypeScript's type checks.


The "Type Narrowing" Trickster

Type narrowing in TypeScript refers to the process of refining the type of a variable within a block of code, based on conditions or operations. While it's a powerful feature for making your code type-safe, it can sometimes behave unexpectedly.

function narrowTypeExample(input: string | number) {
  if (typeof input === 'string') {
    // Within this block, TypeScript knows that input is a string
    console.log(input.toUpperCase()); // Works fine
  } else {
    // TypeScript considers input to be a number here
    console.log(input.toUpperCase()); // Error: Property 'toUpperCase' does not exist on type 'number'
  }
}
Enter fullscreen mode Exit fullscreen mode

In the code above, TypeScript narrows the type of input to string within the first if block, allowing you to call the toUpperCase method. However, in the else block, TypeScript treats input as a number, resulting in a type error when trying to use toUpperCase.


The "Type Inference" Trickster

Type inference is one of TypeScript's strengths, but it can sometimes lead to surprising behavior if you're not careful. TypeScript often infers types from the values you assign to variables, and this can lead to unexpected results.

let x = 5; // TypeScript infers the type of x as number

x = "Hello, TypeScript!"; // Error: Type 'string' is not assignable to type 'number'
Enter fullscreen mode Exit fullscreen mode

In the code above, TypeScript initially infers that x is a number based on the assigned value. However, when you later try to assign a string to x, TypeScript throws an error because it expects x to be of type number. This is where type annotations can be useful to explicitly specify the type of a variable.

let x: number = 5; // Explicitly specifying the type
x = "Hello, TypeScript!"; // Error: Type 'string' is not assignable to type 'number'
Enter fullscreen mode Exit fullscreen mode

The "Tuple vs. Array" Trickster

TypeScript allows you to define both arrays and tuples, but understanding the differences between them is crucial. Arrays are meant for collections of values of the same type, while tuples allow you to specify the types of elements at specific positions.

let array: number[] = [1, 2, 3];
let tuple: [number, string] = [1, "two"];
Enter fullscreen mode Exit fullscreen mode

Here's where the trickster comes in:

let array: number[] = [1, "two", 3]; // No error at compile-time
let tuple: [number, string] = [1, "two", 3]; // Error: Type 'number' is not assignable to type 'string'
Enter fullscreen mode Exit fullscreen mode

In the first case, TypeScript doesn't catch the error because arrays are meant to hold elements of the same type. In the second case, TypeScript correctly identifies the type mismatch because tuples have predefined types for each position.


The "Optional Properties" Trickster

TypeScript allows you to define optional properties in interfaces and object types using the ? syntax.

interface Person {
  name: string;
  age?: number; // Age is an optional property
}

const john: Person = { name: "John" }; // Works fine
Enter fullscreen mode Exit fullscreen mode

This flexibility can lead to subtle bugs if you forget to check for the existence of optional properties:

function greet(person: Person) {
  console.log(`Hello, ${person.name}! You are ${person.age} years old.`);
}

greet(john); // Error: Object is possibly 'undefined'.
Enter fullscreen mode Exit fullscreen mode

In this example, the greet function assumes that the age property exists, but TypeScript raises a type error because age is optional and might not be present on the john object.

To handle optional properties safely, you can use a conditional check or the nullish coalescing operator (??):

function greetSafe(person: Person) {
  console.log(`Hello, ${person.name}! You are ${person.age ?? 'unknown'} years old.`);
}

greetSafe(john); // No error, handles optional property gracefully
Enter fullscreen mode Exit fullscreen mode

I hope you found this blog helpful. If you have any questions, please feel free to leave a comment below. 🤗

I can write a blog like that about any programming language or framework/lib. What would you like me to blog about next time?
Send me a coffee ☕ with what you would like me to write about next time and I will definitely consider your suggestion and if I like your suggestion I will write about your suggestion in the next blog post. 😉

Top comments (3)

Collapse
 
dsaga profile image
Dusan Petkovic

Good one, maybe just the first thing is not specifically a typescript issue, its how javascript works :/

Also the last point "The "Optional Properties" Trickster" I can't quite reproduce, it works fine unless you make age required in the type...

Collapse
 
syeo66 profile image
Red Ochsenbein (he/him)

I think it can happen when you don't have 'strict: true' in your tsconfig.

Collapse
 
dsaga profile image
Dusan Petkovic

It maybe just warns you at compile time, and in javascript it does at run time