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:
- Any typed variable can have
any
value set to it. - 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:
- 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 ofnever
. - Functions That Always Throw: A function with a return type of
never
always throws an error, such asthrow 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.