Frontend Development 17 min read

Six Advanced TypeScript Techniques: Advanced Types, Decorators, Namespaces, Mixins, Type Guards, and Utility Types

This article introduces six advanced TypeScript techniques—advanced types, decorators, namespaces, mixins, type guards, and utility types—providing clear examples and code snippets that demonstrate how to improve code quality, modularity, and type safety in modern JavaScript projects.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Six Advanced TypeScript Techniques: Advanced Types, Decorators, Namespaces, Mixins, Type Guards, and Utility Types

1 — Advanced Types

Advanced types such as mapped types and conditional types let you construct new types from existing ones, giving your code greater flexibility and maintainability.

Mapped Types

Mapped types iterate over the properties of an existing type and apply a transformation, for example creating a readonly version of a type.

type Readonly
= { readonly [P in keyof T]: T[P]; };
interface Point { x: number; y: number; }
type ReadonlyPoint = Readonly
;

The example defines a Readonly mapped type that takes a generic type T and makes all its properties readonly, then creates ReadonlyPoint based on the Point interface.

Conditional Types

Conditional types allow you to create new types based on a condition, using a syntax similar to the ternary operator with the extends keyword.

type NonNullable
= T extends null | undefined ? never : T;

This defines a NonNullable type that removes null and undefined from T . The article then shows how a readonly type cannot be used where a mutable type is expected, preventing accidental side‑effects.

2 — Decorators

Decorators are powerful higher‑order functions that can add metadata or modify the behavior of classes, methods, properties, and parameters.

Class Decorator

function LogClass(target: Function) {
  console.log(`Class ${target.name} was defined.`);
}
@LogClass
class MyClass {
  constructor() {}
}

The LogClass decorator logs the name of the class when it is defined.

Method Decorator

function LogMethod(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log(`Method ${key} was called.`);
}
class MyClass {
  @LogMethod
  myMethod() {
    console.log("Inside myMethod.");
  }
}
const instance = new MyClass();
instance.myMethod();

The LogMethod decorator logs the method name each time the method is invoked.

Property Decorator

function DefaultValue(value: any) {
  return (target: any, key: string) => {
    target[key] = value;
  };
}
class MyClass {
  @DefaultValue(42)
  myProperty: number;
}
const instance = new MyClass();
console.log(instance.myProperty); // 42

The DefaultValue decorator assigns a default value to the decorated property.

Parameter Decorator

function LogParameter(target: any, key: string, parameterIndex: number) {
  console.log(`Method ${key} parameter ${parameterIndex} was called.`);
}
class MyClass {
  myMethod(@LogParameter value: number) {
    console.log(`In myMethod, value = ${value}.`);
  }
}
const instance = new MyClass();
instance.myMethod(5);

The LogParameter decorator logs the index of the parameter when the method is called.

3 — Namespaces

Namespaces provide a way to organize related code and avoid naming collisions by grouping classes, interfaces, functions, and variables.

Defining a Namespace

namespace MyNamespace {
  export class MyClass {
    constructor(public value: number) {}
    displayValue() {
      console.log(`The value is: ${this.value}`);
    }
  }
}

The MyNamespace namespace contains an exported MyClass that can be accessed from outside the namespace.

Using a Namespace

// Fully qualified name
const instance1 = new MyNamespace.MyClass(5);
instance1.displayValue(); // The value is: 5
// Namespace import
import MyClass = MyNamespace.MyClass;
const instance2 = new MyClass(10);
instance2.displayValue(); // The value is: 10

Both fully qualified names and namespace imports are demonstrated.

Nested Namespaces

namespace OuterNamespace {
  export namespace InnerNamespace {
    export class MyClass {
      constructor(public value: number) {}
      displayValue() {
        console.log(`The value is: ${this.value}`);
      }
    }
  }
}
const instance = new OuterNamespace.InnerNamespace.MyClass(15);
instance.displayValue(); // The value is: 15

Nested namespaces allow hierarchical organization of code.

4 — Mixins

Mixins let you compose classes from smaller reusable parts, promoting modularity and code reuse.

Defining a Mixin

class TimestampMixin
any>(Base: TBase) {
  constructor(...args: any[]) {
    super(...args);
  }
  getTimestamp() {
    return new Date();
  }
}

The TimestampMixin adds a getTimestamp method that returns the current date and time.

Using a Mixin

class MyBaseClass {
  constructor(public value: number) {}
  displayValue() {
    console.log(`The value is: ${this.value}`);
  }
}
class MyMixedClass extends TimestampMixin(MyBaseClass) {
  constructor(value: number) {
    super(value);
  }
}
const instance = new MyMixedClass(42);
instance.displayValue(); // The value is: 42
const timestamp = instance.getTimestamp();
console.log(`The timestamp is: ${timestamp}`);

An instance of MyMixedClass has both the base class method and the mixin method.

5 — Type Guards

Type guards narrow the type of a variable within a code block, enabling safe access to type‑specific properties and methods.

Defining a Type Guard

function isString(value: any): value is string {
  return typeof value === "string";
}

The isString function returns a type predicate that narrows value to string when true.

Using a Type Guard

function processValue(value: string | number) {
  if (isString(value)) {
    console.log(`The length of the string is: ${value.length}`);
  } else {
    console.log(`The square of the number is: ${value * value}`);
  }
}
processValue("hello"); // The length of the string is: 5
processValue(42); // The square of the number is: 1764

The example shows how the guard enables different logic for strings and numbers.

6 — Utility Types

Utility types provide shortcuts for transforming existing types into new ones, improving reusability and type safety.

Using Utility Types

interface Person {
  name: string;
  age: number;
  email: string;
}
type PartialPerson = Partial
;
type ReadonlyPerson = Readonly
;
type NameAndAge = Pick
;
type WithoutEmail = Omit
;

Examples of Partial , Readonly , Pick , and Omit are shown.

Partial

const partialPerson: PartialPerson = {
  name: "John Doe",
};

Partial makes all properties optional.

Readonly

const readonlyPerson: ReadonlyPerson = {
  name: "Jane Doe",
  age: 30,
  email: "[email protected]",
};
// readonlyPerson.age = 31; // Error: cannot assign to read‑only property

Readonly makes all properties immutable.

Pick

const nameAndAge: NameAndAge = {
  name: "John Smith",
  age: 25,
};
// nameAndAge.email; // Error: property does not exist

Pick creates a type with only the selected properties.

Omit

const withoutEmail: WithoutEmail = {
  name: "Jane Smith",
  age: 28,
};
// withoutEmail.email; // Error: property does not exist

Omit creates a type by removing specified properties.

Conclusion

The article covered advanced TypeScript topics—including namespaces, advanced types, decorators, mixins, type guards, and utility types—showing how they can be used to write more modular, reusable, and maintainable code while leveraging the language's powerful type system.

TypeScriptUtility TypesdecoratorsAdvanced TypesType GuardsNamespacesMixins
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.