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.