TypeScript: A few things I picked up today
I spent some time today going through the official Typescript docs. Even though I use TypeScript daily, I realized I barely understand how some concepts actually work. I don’t just want to write TypeScript that works. I want to fully understand my typescript code (okay, maybe not fully, but enough to make sense of it lol). Below are some of the things i learnt.
1. Narrowing
Narrowing is the process of refining broad types into more specific ones based on runtime checks. Let’s look at a simple example:
function function printLength(value: string | number): voidprintLength(value: string | numbervalue: string | number) {
var console: ConsoleThe `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.log(value: string | numbervalue.length);}
Our printLength function accepts a parameter that can be either a string or a number. But when we try to use it, TypeScript throws an error.
Why? While strings have a .length property, numbers don’t, so TypeScript can’t guarantee that .length will always be available. We can fix this by adding a type check:
function printLength(value: string | number) {
if (typeof value === "string") {
console.log(value.length);
} else {
console.log(value);
}
}
Now, something interesting happens. Inside the if block, TypeScript automatically narrows value to a string because we’ve confirmed it is one. That’s why value.length no longer causes an error.
The same applies to the else block. TypeScript knows that if value isn’t a string, then it must be a number, so it treats it as such inside that block. Outside the if-else, however, value remains string | number since no further checks have been made.
Here’s another simple example:
function processInput(value: string | number | boolean) {
let result: string | number | boolean;
if (typeof value === "string") {
result = value.toUpperCase(); // Narrowed to string
} else if (typeof value === "number") {
result = value * 2; // Narrowed to number
} else {
result = value ? 1 : 0; // Narrowed to boolean -> mapped to number
}
return result; // TypeScript correctly infers result as string | number
}
I found this one even more interesting because it makes so much sense. At the start, result is initialized with the type string | number | boolean. But when we check the type of result at the return statement, it’s inferred as string | number.
Why? TypeScript examines all possible execution paths and determines the final possible types. Since result is always transformed into either a string or a number, the boolean type is discarded from the union. Regardless of how the function executes, result will never be returned as a boolean, so TypeScript removes it from the final inferred type.
Other Ways to Narrow Types
Up until this point, we have been using the typeof type guard for checks. But there are other ways to narrow types in TypeScript.
Using instanceof (Type Guard for Objects)
function formatDate(value: Date | string) {
if (value instanceof Date) {
console.log(value.toISOString()); // TypeScript knows value is a Date here
} else {
console.log(value.toUpperCase()); // TypeScript knows value is a string here
}
}
Using in Operator (Type Guard for Object Properties)
type Car = { brand: string; drive: () => void };
type Bike = { brand: string; pedal: () => void };
function moveVehicle(vehicle: Car | Bike) {
if ("drive" in vehicle) {
vehicle.drive(); // TypeScript knows vehicle is a Car here
} else {
vehicle.pedal(); // TypeScript knows vehicle is a Bike here
}
}
Using Equality Narrowing
function processInput(value: string | number | null) {
if (value === null) {
console.log("No value provided."); // TypeScript knows value is null here
} else if (value === 0) {
console.log("Zero detected."); // TypeScript knows value is exactly 0 here
} else {
console.log(`Valid input: ${value}`); // TypeScript now knows value is string | number
}
}
There are other ways to narrow types in TypeScript, but these are the most commonly used.
2. Discrimated unions
A discriminated union is a type of union. Union types describe a value that can be one of several types.
type NumberOrString = number | string;
function printNumberOrString(value: NumberOrString) {
console.log(value);
}
In the example above, NumberOrString is a union type because it is formed from two existing types and represents values that can be either a number or a string.
A discriminated union is a special kind of union type where all members share a common property with a literal type that identifies the specific type in the union. This property is known as the discriminator or tag. Let’s look at an example:
type Circle = {
type: "circle";
radius: number;
};
type Square = {
type: "square";
length: number;
};
type Shape = Circle | Square;
function getArea(shape: Shape) {
if (shape.type === "circle") {
return Math.PI * shape.radius ** 2;
}
}
In this example, Shape is a discriminated union because it combines Circle and Square, both of which share a common property: type. The type property acts as the discriminator, allowing TypeScript to determine whether a given shape is a circle or a square. Inside the getArea function, checking shape.type === "circle" confirms that shape is a Circle, so TypeScript allows access to the radius property without any errors.
Let’s look at another example:
type type Bike = {
kind: "bike";
model: string;
color: string;
wheels: number;
}
Bike = {
kind: "bike"kind: "bike";
model: stringmodel: string;
color: stringcolor: string;
wheels: numberwheels: number;
};
type type Car = {
kind: "car";
model: string;
color: string;
wheels: number;
doors: 5;
}
Car = {
kind: "car"kind: "car";
model: stringmodel: string;
color: stringcolor: string;
wheels: numberwheels: number;
doors: 5doors: 5;
};
type type Vehicle = Bike | CarVehicle = type Car = {
kind: "car";
model: string;
color: string;
wheels: number;
doors: 5;
}
Car | type Bike = {
kind: "bike";
model: string;
color: string;
wheels: number;
}
Bike;
const const kawasaki: Vehiclekawasaki: type Vehicle = Bike | CarVehicle = {
kind: "bike"kind: "bike",
model: stringmodel: "Ninja ZX",
color: stringcolor: "black",
wheels: numberwheels: 2,
doors: 0,};
When we try to assign the doors property to kawasaki, TypeScript throws an error. Even though 0 doors is technically correct, the Bike type does not include a doors property, so TypeScript prevents us from adding it.
But how does TypeScript know kawasaki is a bike? The answer lies in the kind property, the discriminator. Since we explicitly set kind: "bike", TypeScript understands that kawasaki must follow the Bike type definition. Adding a doors property contradicts this definition, which is why the error occurs.
3. Structural typing in Typescript
Structural typing is a way of deterniming compatibility of two types based on their atual structure rather than their names. Here’s a simple example:
type Cat = { name: string; meow: () => void };
type Dog = { name: string; bark: () => void };
let pet: Cat = { name: "Whiskers", meow: () => console.log("Meow!") };
let randomPet = { name: "Buddy", meow: () => console.log("Meow!") };
pet = randomPet; // Allowed, because it has the same structure
In the example above even though randomPet is not explicitly a Cat, it fits the structure, so TypeScript allows the assignment.This contrasts with nominal typing, where types are only compatible if explicitly declared as the same type.
Structural typing also allows objects to have additional properties beyond those initially defined in a type. Let’s modify our example:
// ...rest of the code
let randomPet = {
name: "Buddy",
meow: () => console.log("Meow!"),
owner: "John Doe",
};
pet = randomPet;
You might expect this to throw an error since randomPet has an extra owner property that Cat does not define. However, TypeScript still allows the assignment because randomPet contains all the required properties to be considered a Cat. Extra properties do not affect compatibility when assigning an object to a type with fewer properties.
Structural typing makes it easier and more flexible to work with different types that share the same structure.
4. Type aliases and Intersection Types
A type alias is simply a name for a type. It can represent any type, making it easier to reuse and maintain:
type Name = string;
type Age = number;
Here, Name and Age are type aliases for string and number, respectively.
Intersection types allow us to combine multiple types into one using the & operator. This is useful when an object needs to satisfy multiple type definitions at once.
type type Person = {
name: string;
age: number;
}
Person = { name: stringname: string; age: numberage: number };
type type Employee = {
company: string;
role: string;
}
Employee = { company: stringcompany: string; role: stringrole: string };
type type Worker = Person & EmployeeWorker = type Person = {
name: string;
age: number;
}
Person & type Employee = {
company: string;
role: string;
}
Employee; // intersection type
const const worker: Workerworker: type Worker = Person & EmployeeWorker = {
name: stringname: "John",
age: numberage: 23,
company: stringcompany: "TechCorp",
role: stringrole: "Developer",
};
In this example, Worker is an intersection of Person and Employee. This means worker must have all the properties from both types.
5. Generics
Generics in TypeScript allow us to write reusable and flexible code that can work with multiple data types while maintaining type safety.
Take this basic function:
function function printItem(item: any): voidprintItem(item) { var console: ConsoleThe `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.log(item: anyitem);
}
This function works with any value, but TypeScript doesn’t enforce type safety. We could use any to explicitly allow any type, but that defeats the purpose of TypeScript’s type checking. This is where generics come in:
function printItem<Type>(item: Type): Type {
console.log(item);
return item;
}
// Using the generic function
let str = printItem<string>("hello");
let num = printItem<number>(42);
A type parameter <Type> acts as a placeholder for the actual type that will be used. When we define item: Type, it means item will take on whatever type is provided when calling the function. The return type Type ensures that the function returns a value of the same type that was passed in.
For example, calling printItem<string>("hello") tells TypeScript that Type is string, so item must be a string. Similarly, printItem<number>(42) infers that Type is number, meaning item must be a number. This approach allows us to write flexible yet type-safe code that adapts to different data types while maintaining correctness.
A more practical example is:
function function getFirstItem<T>(arr: T[]): TgetFirstItem<function (type parameter) T in getFirstItem<T>(arr: T[]): TT>(arr: T[]arr: function (type parameter) T in getFirstItem<T>(arr: T[]): TT[]): function (type parameter) T in getFirstItem<T>(arr: T[]): TT {
return arr: T[]arr[0];
}
let let firstString: stringfirstString = function getFirstItem<string>(arr: string[]): stringgetFirstItem<string>(["apple", "banana", "cherry"]);
let let firstNumber: numberfirstNumber = function getFirstItem<number>(arr: number[]): numbergetFirstItem<number>([10, 20, 30]);
var console: ConsoleThe `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.log(let firstString: stringfirstString); // "apple"
var console: ConsoleThe `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.log(let firstNumber: numberfirstNumber); // 10
Here, T[] ensures that the function takes an array of any type, and T guarantees that the returned value matches the type of the array elements.
We are not limited to using a single type parameter. Generics allow us to define multiple type variables for even greater flexibility.
function function transform<Input, Output>(value: Input, transformer: (val: Input) => Output): Outputtransform<function (type parameter) Input in transform<Input, Output>(value: Input, transformer: (val: Input) => Output): OutputInput, function (type parameter) Output in transform<Input, Output>(value: Input, transformer: (val: Input) => Output): OutputOutput>(
value: Inputvalue: function (type parameter) Input in transform<Input, Output>(value: Input, transformer: (val: Input) => Output): OutputInput,
transformer: (val: Input) => Outputtransformer: (val: Inputval: function (type parameter) Input in transform<Input, Output>(value: Input, transformer: (val: Input) => Output): OutputInput) => function (type parameter) Output in transform<Input, Output>(value: Input, transformer: (val: Input) => Output): OutputOutput
): function (type parameter) Output in transform<Input, Output>(value: Input, transformer: (val: Input) => Output): OutputOutput {
return transformer: (val: Input) => Outputtransformer(value: Inputvalue);
}
const const lengthOfString: numberlengthOfString = function transform<string, number>(value: string, transformer: (val: string) => number): numbertransform<string, number>(
"TypeScript",
(str: stringstr) => str: stringstr.String.length: numberReturns the length of a String object.length
);
var console: ConsoleThe `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.log(const lengthOfString: numberlengthOfString); // 10
const const booleanToString: stringbooleanToString = function transform<boolean, string>(value: boolean, transformer: (val: boolean) => string): stringtransform<boolean, string>(true, (bool: booleanbool) =>
bool: booleanbool ? "Yes" : "No"
);
var console: ConsoleThe `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.log(const booleanToString: stringbooleanToString); // "Yes"
In this example, transform<Input, Output> takes two type variables:
Inputrepresents the type of the input value.Outputrepresents the type of the returned value.
The function accepts a value of type Input and a transformer function that converts it into Output. This ensures type safety while keeping the function highly adaptable.
Generics constraints
Sometimes, we want a function to work with any type, but only if that type meets certain criteria. For example, let’s say we want to return the length of an item:
function function getLength<T>(val: T): numbergetLength<function (type parameter) T in getLength<T>(val: T): numberT>(val: Tval: function (type parameter) T in getLength<T>(val: T): numberT): number {
return val: Tval.length;}
The issue here is that TypeScript doesn’t know if T has a .length property. To fix this, we add a constraint using the extends keyword:
function function getLength<T extends {
length: number;
}>(val: T): number
getLength<function (type parameter) T in getLength<T extends {
length: number;
}>(val: T): number
T extends { length: numberlength: number }>(val: T extends { length: number; }val: function (type parameter) T in getLength<T extends {
length: number;
}>(val: T): number
T): number {
return val: T extends { length: number; }val.length: numberlength; // Now TypeScript knows 'length' exists
}
This fixes the error and ensures type safety, allowing only items with a .length property, such as arrays and strings, to be passed as arguments. The example below throws an error because numbers do not have a .length property.
function function getLength<T extends {
length: number;
}>(val: T): number
getLength<function (type parameter) T in getLength<T extends {
length: number;
}>(val: T): number
T extends { length: numberlength: number }>(val: T extends { length: number; }val: function (type parameter) T in getLength<T extends {
length: number;
}>(val: T): number
T): number {
return val: T extends { length: number; }val.length: numberlength;
}
const const numLength: numbernumLength = function getLength<{
length: number;
}>(val: {
length: number;
}): number
getLength(65);
As i dived deeper into the official documentation on generics, i thought i was getting the hang of it, then suddenly, nothing made sense. I took that as my cue to call it a day. Generics are vast and have a steep learning curve, but I’m looking forward to making more sense of them over time.
Wrapping up
Overall, I got a better understanding of the reasoning behind some of the concepts I use. It also made me realize just how much I still have to learn. But that’s all part of the process, figuring things out one step at a time.
Further reading
These sources proved helpful while I was covering the topics in this article:
- Official Typescript Docs
- FreeCodeCamp: How Generics Work
- Prismic: Understanding TypeScript Generics
- TypeScript — Discrimated Unions in TypeScript by Krzysztof Malec