Page Content

Tutorials

What is a Tuple Type in TypeScript? With Examples

Tuple Type in TypeScript

A particular kind of array with a set number of members is called a tuple type, and the values at each index have distinct, known, and perhaps distinct types. Because tuples are not first-class supported in standard JavaScript, arrays are frequently used instead. In order to solve this, TypeScript provides tuple-specific syntax and type checking.

Definition and Structure

The notation :[typeofmember1, typeofmember2] etc. is used to annotate tuples, which can contain any number of members. A tuple type indicates the type for every location, and the order of the types is crucial, in contrast to a typical array type (which presumes homogeneity, such as number[]).

For example, you might define a structure as follows if you wanted a string (a name) as the first element and a number (a numerical value) as the second:

// Code Example: Tuple Type Definition
var nameNumber: [string, number];

This means that an assignment must match both the length and the type order defined in the tuple structure.

// Code Example: Valid Assignment
nameNumber = ['Jenny', 8675309]; // Okay 

// Code Example: Invalid Assignment (Type mismatch)
nameNumber = ['Jenny', '867-5309']; // Error! 

Output (Expected Errors): The second assignment would result in a compile-time error: Error! Type 'string' is not assignable to type 'number'.

Tuples as Subtypes of Arrays

Tuples are regarded as array subclasses. Because they securely encapsulate heterogeneous lists and capture the fixed length of the list, they provide noticeably higher security than ordinary arrays. [number, number, number] is an example of a tuple of three numbers.

Unless specifically stated, TypeScript assumes a flexible size array and handles produced arrays as variable length arrays rather than tuples. As a result, when defining a tuple, explicit type annotation is frequently required.

Tuple Operations and Features

Although tuples can perform operations similar to those of arrays, their modifications are limited by the types that are defined. When working with tuples, TypeScript allows array destructuring, which gives them a first-class feel even if they are arrays underneath:

// Code Example: Tuple Destructuring
var nameNumber: [string, number];
nameNumber = ['Jenny', 8675309]; 
var [name, num] = nameNumber;
console.log(name);
console.log(num);

Output:

Jenny
8675309

The question mark (?) is another way that tuples support optional items.

Any Type

As the ultimate top type (universal type) and a “escape hatch” from the type system, the Any Type (any) has a particular place in TypeScript. A dynamic kind is indicated by it.

Compatibility and Safety

Any type in the type system can be used with any and all other types. This implies:

  1. Any typed variable can have any value set to it.
  2. You can assign any typed variable to anything because it works with all types.

TypeScript does not restrict operations in any way if a value of the type any. TypeScript doesn’t do any useful static analysis on any values; this behaviour is comparable to that of standard JavaScript. Any related expressions are specifically not type-checked.

// Code Example: Any Type Compatibility
var power: any; // Declared as any 

// 1. Takes any and all types
power = '123'; // Assigned string 
power = 123;   // Assigned number

// 2. Is compatible with all types
var num: number = 5;
power = num;   // Okay (number assigned to any) 
num = power;   // Okay (any assigned to number) 

Usage and Recommendation

A large portion of the type checker is circumvented when any is used. Generally speaking, the any kind should be avoided and reserved for extreme circumstances. When migrating existing JavaScript code to TypeScript, developers frequently rely heavily on any at first, although doing so entails manually handling type safety.

The compiler setting noImplicitAny stops TypeScript from defaulting to an implicit any type by telling it to raise an error if it is unable to determine the type of a variable. To validate the deliberate avoidance of type verification, a developer must explicitly annotate a variable as : any if this flag is enabled.

Unknown Type

All other types in TypeScript are subtypes of the Unknown Type (unknown), which is another universal or top type. It stands for any value. On the other hand, unknown is extremely limiting and a type-safe variant of any.

Safety through Restriction

A variable of type unknown can have any value assigned to it, but unlike any other variable, an unknown value cannot be allocated to any type; it can only be assigned to itself or other top types.

Importantly, unless the type is initially narrowed through particular checks, TypeScript does not provide member access or direct manipulation on a value typed as unknown. Any runtime errors that would be permitted are prevented by this restriction.

// Code Example: Unknown Safety (Compile-time errors demonstrated)
function func(value: unknown) {
    // Error: Cannot perform operations assuming a type
    // @ts-expect-error: 'value' is of type 'unknown'.
    value.toFixed(2); // Error! 

    // Error: Cannot assign to non-unknown type
    // @ts-expect-error: Type 'unknown' is not assignable to type 'boolean'.
    const b: boolean = value; // Error! 

    // Type assertion or narrowing is required
    (value as number).toFixed(2); // OK via type assertion 
}

Output (Expected Error Trace): The compiler would issue errors preventing operations like value.toFixed(2) until checks are performed.

Narrowing Unknown

Developers must narrow the type using type assertions or type guards (such as typeof or instanceof) in order to use an unknown value.

// Code Example: Narrowing Unknown
let a: unknown = 30; // unknown 

if (typeof a === 'number') {
  let d = a + 10; // d is number here 
}

Unknown is typically chosen over all other options when working with data of an initially uncertain type since this structural limitation guarantees that developers handle the type explicitly before interacting with the value.

Union Types

When you want a variable or property to be able to retain a value that belongs to more than one specified type, you utilise union types.

Syntax and Structure

To declare union types, use the pipe symbol (|). The set-theoretic union (sum) of the sets of values described by the constituent types is represented by the resultant type.

A function argument that takes a single object or an array of objects, or a variable that can be a string or a number, is a typical JavaScript use case:

// Code Example: Union Type Declaration
function formatCommandline(command: string[] | string) { // 
    var line = '';
    if (typeof command === 'string') {
        line = command.trim();
    } else {
        line = command.join(' ').trim();
    }
    // ...
}

// Code Example: Variable Union
var val: string | number; // 
val = 12; // numeric value 
val = "This is a string"; // string value 

Type Narrowing and Guards

If a variable has a union type, TypeScript will only allow access to methods or properties that are shared by all union members. Developers must narrow the type inside a conditional block using type guards in order to access type-specific members.

TypeScript recognises the usage of typeof to limit the type in conditional statements.

// Code Example: Union Type Narrowing with typeof
function doSomething(x: number | string) { // 
    if (typeof x === 'string') {
        // Within this block, TypeScript knows `x` must be a string
        console.log(x.substr(1)); // OK 
    }
    // Outside the block, no guarantee that `x` is a `string` 
}

Handling Null and Undefined

Null and undefined are not automatically included in normal types if strict null checks are enabled; instead, they must be inserted explicitly using union types (such as Element | null).

String Literal Types

Types that are specified by a single, precise string value are known as string literal types. They enable you to specify a variable that is limited to storing that particular string value.

Definition and Constraints

Essentially limited to that literal, a variable declared with a string literal type constitutes a limited subset of the broader string primitive type:

// Code Example: String Literal Type Definition
let myFavoritePet: "dog"; // 
myFavoritePet = "dog";    // Ok

// Error due to assignment constraint:
// myFavoritePet = "rock"; // Error: Type '"rock"' is not assignable to type '"dog"'. 

Output (Expected Error): Attempting to assign "rock" results in a compile-time error.

Power through Union

A single string literal type is constrictive, but when joined with other literal kinds to form a union type, its full potential is shown. You may effectively simulate a string-based enum using this technique.

By requiring that only a particular, known set of strings be used, this combination produces a strong abstraction that is great for developer experience and safety (e.g., offering autocomplete):

// Code Example: String Literal Union (Enum-like behavior)
type CardinalDirection =
    | "North"
    | "East"
    | "South"
    | "West"; // 

function move(distance: number, direction: CardinalDirection) {
    // ... implementation
}

move(1, "North"); // Okay 
// move(1, "Nurth"); // Error! 

Output (Expected Error): The call move(1, "Nurth") produces an error because "Nurth" is not assignable to type CardinalDirection.

Another important feature of discriminated unions is string literal types, where a literal property value serves as a hook for TypeScript to specify the particular object type inside a union.

Type Aliases

Any type annotation can be given a name (an alias) using Type Aliases, which makes it easy to reuse across the codebase. The type keyword, alias name, and type definition are used in their creation.

Flexibility and Usage

Type aliases can be applied to almost any type annotation, including complicated kinds like union types, intersection types, and tuple types, in contrast to interfaces, which are mostly used for describing object forms.

Type aliases come in handy for:

Reusability and Clarity: By giving complex type annotations semantic names, they improve readability and aid in the elimination of duplication (DRY idea).

Naming Complex Types: This is how union or intersection types are typically named. Naming a union of primitives, for instance:

// Code Example: Type Alias for a Union Type
type StrOrNum = string | number; 

var sample: StrOrNum; // Usage: just like any other notation 
sample = 123;
sample = '123';
// sample = true; // Error! Type 'boolean' is not assignable to type 'StrOrNum'.

3. Naming Tuples:

// Code Example: Type Alias for a Tuple Type
type Coordinates = [number, number];

Note on Duplication: One important distinction from interfaces is that you can’t specify a type alias more than once; doing so causes an error called Duplicate Identifier.

Never type

In TypeScript’s hierarchy, the Never Type (never) is the lowest type (also known as a falsum) and denotes a type with no values.

When Never Occurs

Code locations that are never reached during execution are represented by the never type. It happens organically in a number of situations:

  1. Functions That Never Return: Because an endless loop (while(true){}) never ends gracefully, a function with this sort of loop will have a return type of never.
  2. Functions That Always Throw: A function with a return type of never always throws an error, such as throw new Error().
// Code Example: Function Returning Never (always throws)
function fail(message: string): never { 
    throw new Error(message); 
} // 

// The result of this immediately invoked function expression is assignable to never:
let bar: never = (() => {
    throw new Error('Throw my hands in the air like I just dont care');
})(); // Okay 

Assignability

You can only assign one never to another. On the other hand, never can be assigned to any other type since it is a subtype of all other types. A compiler error occurs when you try to assign anything else to a variable that is typed as never:

// Code Example: Invalid assignment to never
let foo: never; // Okay 
// let foo: never = 123; // Error: Type number is not assignable to never 

Output (Expected Error): Error: Type number is not assignable to never.

Use Case: Exhaustive Checks

Extensive checks, also known as totality checks, are the most important use case for never, especially in control flow analysis using union types.

TypeScript narrows down types in conditional blocks by analysing code flow. The type of a variable in the last else or default block is limited to never if it is a union type and you use if/else if or switch statements to check every potential member of that union.

By using this approach, you can confirm at compilation time that every union member has been dealt with. If any case was overlooked, TypeScript will produce an error when it tries to assign the variable in the last, purportedly inaccessible, block to another variable that is explicitly typed as never:

// Setup: Assuming Shape = Square | Rectangle | Circle (where Circle was just added) 
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle; // 

function area(s: Shape) {
    if (s.kind === "square") {
        // handle square
    } else if (s.kind === "rectangle") {
        // handle rectangle
    } else {
        // If Circle case is missing:
        // ERROR: `Circle` is not assignable to `never` 
        const _exhaustiveCheck: never = s;
    }
}

If the developer forgets to handle the Circle case, in the final else block, s is inferred as Circle. Trying to assign s (type Circle) to _exhaustiveCheck (type never) produces a compile-time error, forcing the developer to handle the new Circle type. Once the Circle case is handled, the else block is truly unreachable, s becomes never, and the code compiles successfully.

Index