Step 14 – Advanced TypeScript Features

TypeScript offers several advanced features that enhance type safety and flexibility in your code. In this step, we’ll explore Conditional Types, Mapped Types, and Type Aliases, providing comparisons with Java where applicable.

Conditional Types

Conditional types enable you to create types based on conditions. The syntax is T extends U ? X : Y, where:

  • T is the type being checked.
  • U is the condition to test against.
  • X is the type if the condition is true.
  • Y is the type if the condition is false.

TypeScript Example:

type IsArray<T> = T extends Array<any> ? true : false;
type MyType = IsArray<number>; // MyType is false
type MyOtherType = IsArray<number[]>; // MyOtherType is true

In this example, IsArray checks if T is an array. If T is an array, it returns true; otherwise, it returns false.

Java Comparison:

Java does not have a direct equivalent of conditional types, but similar functionality can be achieved using generics and type checks.

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        System.out.println(isArray(123)); // false
        System.out.println(isArray(Arrays.asList(1, 2, 3))); // true
    }

    static boolean isArray(Object obj) {
        return obj instanceof List;
    }
}

Here, isArray checks if the object is an instance of List, which is similar to checking if a type is an array in TypeScript.

Mapped Types

Mapped types create new types by iterating over properties of an existing type. The syntax is { [P in K]: T }, where:

  • P is the property key.
  • K is a union of property keys.
  • T is the type of the property.

TypeScript Example:

interface MyInterface {
  foo?: number;
  bar?: string;
}

type RequiredMyInterface = { [P in keyof MyInterface]-?: MyInterface[P] };

In this example, RequiredMyInterface is a mapped type that makes all properties of MyInterface required.

Java Comparison:

Java does not have built-in mapped types, but similar effects can be achieved using design patterns or libraries. For example, you could use a combination of interfaces and classes to ensure that all fields are set:

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        MyObject obj = new MyObject(1, "hello");
        System.out.println(obj.getFoo()); // 1
        System.out.println(obj.getBar()); // hello
    }
}

class MyObject {
    private final int foo;
    private final String bar;

    public MyObject(int foo, String bar) {
        this.foo = foo;
        this.bar = bar;
    }

    public int getFoo() {
        return foo;
    }

    public String getBar() {
        return bar;
    }
}

In this example, MyObject ensures that foo and bar are required fields.

Type Aliases

Type aliases provide a way to create a new name for an existing type. The syntax is type NewType = OldType.

TypeScript Example:

type Callback<T> = (arg: T) => void;
type NewCallback<T> = (arg: T, callback: Callback<T>) => void;

function addCallback<T>(fn: (arg: T) => void): NewCallback<T> {
  return (arg, callback) => {
    fn(arg);
    callback(arg);
  };
}

Here, Callback and NewCallback are type aliases that define callback function types.

Java Comparison:

Java does not have direct type aliasing like TypeScript but uses interfaces and functional interfaces for similar purposes.

import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        Consumer<Integer> callback = x -> System.out.println("Callback with: " + x);
        addCallback(x -> System.out.println("Processing: " + x), callback);
    }

    static void addCallback(Consumer<Integer> fn, Consumer<Integer> callback) {
        fn.accept(5);
        callback.accept(5);
    }
}

In this example, Consumer<Integer> serves a similar purpose to TypeScript’s type aliases for callback functions.

Text Diagram

To visualize how these advanced features work:

Conditional Types:

IsArray<T>:
T extends Array<any> ? true : false

Mapped Types:

RequiredMyInterface:
{ [P in keyof MyInterface]-?: MyInterface[P] }

Additional Code Examples

Conditional Types:

type IfEquals<T, U> = [T] extends [U] ? true : false;

type MyType = IfEquals<number, number>; // MyType is true
type MyOtherType = IfEquals<number, string>; // MyOtherType is false

Mapped Types:

interface MyObject {
  foo: number;
  bar: string;
}

type ReadonlyMyObject = { readonly [P in keyof MyObject]: MyObject[P] };

const obj: ReadonlyMyObject = { foo: 1, bar: "hello" };
obj.foo = 2; // Error: Cannot assign to 'foo' because it is a read-only property

Type Aliases:

type User = {
  id: number;
  name: string;
};

type UserID = User["id"];

const user: User = { id: 1, name: "John" };
const userID: UserID = user.id; // userID is of type number

Conclusion

Advanced TypeScript features like Conditional Types, Mapped Types, and Type Aliases offer powerful ways to manage and manipulate types. Understanding these features helps you write more flexible and maintainable code. While Java doesn’t provide direct equivalents, similar patterns and practices can be used to achieve comparable results.


Leave a Reply

Your email address will not be published. Required fields are marked *