TypeScript Concepts: A Deep Dive with Examples

Hossein Mohammadi
5 min readJan 21, 2024

--

This article is a comprehensive exploration of TypeScript, aimed at both beginners and experienced developers. Throughout the article, readers will gain a thorough understanding of TypeScript’s key concepts, Also we will describe complex types in the real-world. Each concept is accompanied by practical examples, equipping readers with the knowledge and tools to leverage TypeScript effectively in their own projects. Whether you’re new to TypeScript or seeking to deepen your understanding, this article will provide valuable insights and practical guidance for mastering TypeScript concepts.

Look at this Code


export type IsEqual<T1, T2> = T1 extends T2
? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2
? true
: false
: false;

type AnyIsEqual<T1, T2> = T1 extends T2
? IsEqual<T1, T2> extends true
? true
: never
: never;

type PathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? `${K}`
: // Check so that we don't recurse into the same type
// by ensuring that the types are mutually assignable
// mutually required to avoid false positives of subtypes
true extends AnyIsEqual<TraversedTypes, V>
? `${K}`
: `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`;

type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[TupleKeys<T>]
: PathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[keyof T];

/**
* Type which eagerly collects all paths through a type
* @typeParam T - type which should be introspected
* @example
* ```
* Path<{foo: {bar: string}}> = 'foo' | 'foo.bar'
* ```
*/
// We want to explode the union type and process each individually
// so assignable types don't leak onto the stack from the base.
export type Path<T> = T extends any ? PathInternal<T> : never;

/**
* See {@link Path}
*/
export type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>;


type ArrayPathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? IsAny<V> extends true
? string
: never
: V extends ReadonlyArray<infer U>
? U extends Primitive | BrowserNativeObject
? IsAny<V> extends true
? string
: never
: // Check so that we don't recurse into the same type
// by ensuring that the types are mutually assignable
// mutually required to avoid false positives of subtypes
true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}` | `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`
: true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`;

This segment of the react-hook-form source code showcases robust TypeScript typings, offering valuable insights for mastering advanced TypeScript concepts.

Type Aliases:

The ‘type’ keyword serves as a versatile tool for defining custom types, enabling developers to create aliases for existing types and construct complex data structures with specific shapes and compositions. By using the ‘type’ keyword, developers can enhance code readability and maintainability, while also leveraging features such as union types, intersection types, and conditional types.

type UserID = number;
type Username = string;

// Usage
let userId: UserID = 123;
let username: Username = "john_doe";

// Object
type Car = {
make: string;
model: string;
year: number;
};

// Usage
let myCar: Car = {
make: "Toyota",
model: "Camry",
year: 2020
};

// Union
type Status = "active" | "inactive";

// Usage
let userStatus: Status = "active";

Unions (|) and intersections (&):

Unions:

type Vehicle = "Car" | "Bicycle" | "Motorcycle";

function getVehicleDescription(vehicle: Vehicle): string {
switch (vehicle) {
case "Car":
return "Four-wheeled vehicle";
case "Bicycle":
return "Two-wheeled, human-powered vehicle";
case "Motorcycle":
return "Two-wheeled motorized vehicle";
default:
return "Unknown vehicle type";
}
}

Intersections:

type Employee = {
id: number;
name: string;
};

type Role = {
role: string;
};

type Developer = Employee & Role;

const softwareEngineer: Developer = {
id: 1,
name: "John Doe",
role: "Software Engineer",
};

Nullable Type:

function findUserById(id: number): User | null {
// Search for a user by ID and return it if found; otherwise, return null
}

const user = findUserById(123);

if (user !== null) {
console.log(`User found: ${user.name}`);
} else {
console.log("User not found");
}

The unknown Type:

let userInput: unknown;

if (typeof userInput === "string") {
console.log(userInput.toUpperCase());
} else {
console.log("User input is not a string");
}

‘is’:

function isObject(x: unknown): x is object {
return typeof x === "object" && x !== null && !Array.isArray(x);
}

// Usage
function processInput(data: unknown) {
if (isObject(data)) {
// 'data' is now narrowed to type 'object'
// Perform operations specific to objects
console.log("Data is an object:", data);
} else {
// 'data' is still of type 'unknown' here
console.log("Data is not an object");
}
}

// Example usage
processInput({ name: "John", age: 30 }); // Output: Data is an object: { name: "John", age: 30 }
processInput("Hello, world"); // Output: Data is not an object

Never type:

Certainly! The never type in TypeScript is a type that represents values that never occur. It is often used in conditional types to express situations where a certain branch of the type should never be reached or produce a value.

type NonNullable<T> = T extends null | undefined ? never : T;

// Example usage
let variable1: string | null = "Hello";
let variable2: number | undefined = 42;
let variable3: null | undefined;

let result1: NonNullable<typeof variable1>; // Result: string
let result2: NonNullable<typeof variable2>; // Result: number
let result3: NonNullable<typeof variable3>; // Result: never (as it can be null or undefined)

It’s time to act.

Let’s collaboratively code a type-safe navigation service with ‘navigate’ and ‘goBack’ functions that intelligently handle parameters for each page.

class NavigationService<ParamListT extends Record<string, object | undefined>> {
navigate<KeyT extends keyof ParamListT, Key2T extends Exclude<keyof ParamListT, KeyT>>(
name: KeyT,
params?: { screen?: { name: Key2T; params?: ParamListT[Key2T] } } | ParamListT[KeyT]
) {
name;
params;
}

goBack() {}
}

const navigationService = new NavigationService<{
Home: undefined;
Product: { id: string };
Comment: { id: string };
}>();

navigationService.navigate('Product', { id: '', screen: { name: 'Home' } });

ParamListT Type Parameter:

This is a generic type parameter for the NavigationService class. It extends Record<string, object | undefined>, which means it must be an object type where keys are strings (navigation route names), and values (navigation route parameters) are either objects or undefined.

navigate Method:

KeyT and Key2T Type Parameters:

KeyT represents the keys of the ParamListT type (navigation route names). Key2T represents keys that can be used in nested navigate.

name Parameter:

It must be of type KeyT, which means it should be a valid key of the ParamListT type.

params Parameter:

It’s optional and can have two types:

{ screen?: { name: Key2T; params?: ParamListT[Key2T] } }: An object with a screen property that has a name property of type Key2T and an optional params property of type ParamListT[Key2T].

ParamListT[KeyT]: Directly the value associated with the key name in the ParamListT type. Presenting parameters of route.

End

I hope this article could help you typescript developers. Please help me to edit it.

--

--

Hossein Mohammadi
Hossein Mohammadi

No responses yet