Page Content

Tutorials

What are Intersection types in TypeScript?

Intersection types in TypeScript

Members of two or more kinds are combined in an intersection type. “And” can be read as the ampersand operator (&), which is used to represent this idea. In essence, an intersection type calculates the types’ set-theoretic intersection.

Purpose and Functionality

The primary inspiration for intersection types comes from a common JavaScript pattern known as the extend pattern, which involves taking two objects and constructing a new one with the properties or capabilities of both. Developers can apply this extension pattern in a type-safe way by using intersection types.

All of the properties specified by the constituent types are present in the final intersection type when applied to object types. For example, the intersection type $A & B$ requires all properties from $A$ and all properties from $B$ if you have type $A$ and type $B$. Since an item must meet the requirements of each type in the intersection, this narrows the set of compatible values in terms of set theory.

Modelling shared characteristics in one location is another advantage of intersection kinds. For instance, individual fields for several subtypes can cross with a base type that contains shared fields. Additionally, every type annotation can be assigned a type alias, which is particularly helpful for naming intricate structures like intersection types.

Resolving conflicting index signatures in object types is one particular use for intersection types. An intersection type can be used as a workaround when a type requires both fixed named properties and an index signature, which may conflict in a standard definition:

type FieldState = {
    value: string
}
// Workaround using an intersection type:
type FormState =
    { isValid: boolean } // Fixed property
    & { [fieldName: string]: FieldState } // Index signature

Code Example: Extending Objects

The following example demonstrates a generic extend function that uses an intersection type in its return signature to safely guarantee that the result combines the properties of the two input objects:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U> {};
    for (let id in first) {
        result[id] = first[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            result[id] = second[id];
        }
    }
    return result as T & U;
}

var x = extend({ a: "hello" }, { b: 42 });
// x now has both `a` and `b`
var a = x.a; // Type inferred as string
var b = x.b; // Type inferred as number

Output (Conceptual Result in x):

{ a: "hello", b: 42 }

Code Example: Combining Capabilities

Intersection types are also ideal for defining composite utility types, such as defining a tool that must possess multiple interfaces:

interface Knife { cut(): void; }
interface BottleOpener{ openBottle(): void; }
interface Screwdriver{ turnScrew(): void; }

type SwissArmyKnife = Knife & BottleOpener & Screwdriver;

function use(tool: SwissArmyKnife){
    console.log("I can do anything!");
    tool.cut();
    tool.openBottle();
    tool.turnScrew();
}

// Example usage demonstrating capability enforcement
const myTool: SwissArmyKnife = {
    cut: () => console.log("Cutting..."),
    openBottle: () => console.log("Opening bottle..."),
    turnScrew: () => console.log("Turning screw...")
};
use(myTool);

Output:

I can do anything!
Cutting...
Opening bottle...
Turning screw...

Type Guards

TypeScript can refine or restrict the type of an object within that scope by using Type Guards, which are conditional blocks. They are a particular kind of type inference that is applied inside a code block. Type guards are necessary to distinguish between the various types of union types (values that may be one of two or more alternative types) in order to carry out particular, type-safe operations.

Several built-in JavaScript operators and constructs that serve as type guards are recognised by TypeScript’s type checker:

The typeof operator is used to distinguish between JavaScript primitive types: number, string, boolean, and symbol. If typeof is used in a conditional block, TypeScript understands that the variable inside that block must conform to that specific type.

To find out if an object is an instance of a specific class, use the instanceof operator with classes. TypeScript restricts the variable’s type inside the conditional scope when instanceof is used as a guard. If one type is ruled out, TypeScript is smart enough to narrow the type in the next else block as well.

The in operator can be used as a type guard to enable narrowing within structural types and safely check for the existence of a property on an object.

Literal Type Guards

When types in a union share a property defined by a literal type (the discriminant or type tag), TypeScript automatically narrows the type and discriminates between the union members by testing the value of this property (===,!==, or switch).

Code:

function doSomething(x: number | string) {
    if (typeof x === 'string') { // Narrowing x to string
        console.log(`String operation: ${x.substr(1)}`);
    }
}

class Foo { foo = 123; }
class Bar { bar = 456; }

function doStuff(arg: Foo | Bar) {
    if (arg instanceof Foo) {
        console.log(`Foo property: ${arg.foo}`); // OK
    }
    if (arg instanceof Bar) {
        console.log(`Bar property: ${arg.bar}`); // OK
    }
}

doSomething('hello');
doStuff(new Foo());
doStuff(new Bar());

Output:

String operation: ello
Foo property: 123
Bar property: 456

User-Defined Type Guards

You can develop User-Defined Type Guard methods in situations involving complicated structural typing when JavaScript does not have rich runtime introspection (i.e., instanceof or simple typeof tests are not possible).

A type predicate, which is a specific return type, is used to declare these functions: someArgumentName is SomeType. The function body, which returns a boolean, is run during runtime, while the compiler narrows types at compile time using the type predicate. TypeScript confidently narrows the type inside the guarded block if the function returns true.

Code:

interface Foo { foo: number; common: string; }
interface Bar { bar: number; common: string; }

// User Defined Type Guard function
function isFoo(arg: any): arg is Foo {
    return arg.foo !== undefined;
}

function handleStuff(arg: Foo | Bar) {
    if (isFoo(arg)) {
        console.log(`It is Foo with value: ${arg.foo}`); // TypeScript knows 'arg' is Foo
    } else {
        console.log(`It must be Bar with value: ${arg.bar}`); // TypeScript knows 'arg' is Bar
    }
}

handleStuff({ foo: 456, common: 'hello' });
handleStuff({ bar: 789, common: 'world' });

Output:

It is Foo with value: 456
It must be Bar with value: 789

Type Assertions

A feature called Type Assertion lets you tell the TypeScript compiler what a variable is, thus overriding its inferred or analysed view. By doing this, you are indicating to the compiler that you understand the type structure better than its internal analysis.

Syntax and Comparison

The as foo style is the recommended syntax for type assertion because the previous angle bracket style () created ambiguities, especially in JSX/TSX syntax.

The term “assertion” is frequently used instead of “casting” (which typically implies runtime support) because a type assertion is purely a compile-time construct. An escape route from the type system is provided by assertions.

Safety and Rules

Since type assertions are dangerous by nature and can circumvent TypeScript’s safety mechanisms, they should be avoided whenever possible.

Single assertions are subject to safety limitations enforced by TypeScript: an assertion from type $S$ to type $T$ can only succeed if either $S$ or $T$ is a subtype of $S$. TypeScript will raise a compilation error if the types are wholly incompatible (for example, asserting Event as HTMLElement without a shared structural connection).

By first asserting the value to any, which is compatible with all kinds, and then asserting to the chosen target type (e.g., value as any as TargetType), one can completely circumvent this safety check.

Non-Null Assertions ()

Removing null or undefined from a type that is theoretically nullable but practically guaranteed to exist is a particular, frequent use case for assertion. The non-null assertion operator (!) is the abbreviation for this. Setting up! when the compiler is instructed by a variable to treat the type as its non-nullable form (for example, T | null becomes T).

Code Example: Assertion and Double Assertion

interface Foo {
    bar: number;
    bas: string;
}

// 1. Basic Type Assertion (as syntax)
var foo = {} as Foo; // Telling the compiler that an empty object satisfies Foo
foo.bar = 123;
foo.bas = 'hello';

console.log(foo);


// 2. Double Assertion Example (to bypass type incompatibility check)
// Assume Event and HTMLElement are unrelated types that cause an error:
// function handler(event: Event) {
//     let element = event as HTMLElement; // Error: Not assignable
// } 

function handler(event: Event) {
    // Asserting first to 'any' bypasses the safety check
    let element = event as any as HTMLElement; // Okay!
    
    // Demonstrate compiler acceptance
    console.log("Compiler accepted double assertion successfully.");
}

Output:

{ bar: 123, bas: 'hello' }
Compiler accepted double assertion successfully.
Index