I/sindresorhus/is

sindresorhus/is Codebase Documentation

Architecture Overview

The sindresorhus/is codebase is structured around a central module that provides a comprehensive suite of type-checking functions. The primary objective of this module is to determine the type of a given value, supporting a wide array of JavaScript types including primitives, objects, and complex constructs like Promises and TypedArrays.

The codebase is organized into several key files within the source directory:

  • index.ts: This is the main entry point of the module. It contains the core logic for type detection and exports a variety of functions for checking different types.
  • types.ts: This file defines TypeScript types and interfaces that are used throughout the codebase. These types encapsulate the expected structures and constraints of various data types.
  • utilities.ts: This file includes utility functions that support the main type-checking logic.

The design follows a modular approach where each file has a specific responsibility, contributing to the overall functionality of the type-checking module.

Core Data Structures and Abstractions

Type Definitions

The types.ts file defines several key types that are used throughout the codebase:

export type Primitive =
    | null
    | undefined
    | string
    | number
    | boolean
    | symbol
    | bigint;

export type Class<T, Arguments extends unknown[] = any[]> = Constructor<T, Arguments> & {prototype: T};

export type TypedArray =
    | Int8Array
    | Uint8Array
    | Uint8ClampedArray
    | Int16Array
    | Uint16Array
    | Int32Array
    | Uint32Array
    | Float32Array
    | Float64Array
    | BigInt64Array
    | BigUint64Array;

export type ObservableLike = {
    subscribe(observer: (value: unknown) => void): void;
    [Symbol.observable](): ObservableLike;
};

export type Falsy = false | 0 | 0n | '' | null | undefined;

export type WeakRef<T extends object> = {
    readonly [Symbol.toStringTag]: 'WeakRef';
    deref(): T | undefined;
};

export type ArrayLike<T> = {
    readonly [index: number]: T;
    readonly length: number;
};

export type NodeStream = {
    pipe<T extends NodeJS.WritableStream>(destination: T, options?: {end?: boolean}): T;
} & NodeJS.EventEmitter;

export type Predicate = (value: unknown) => boolean;

export type NonEmptyString = string & {0: string};

export type Whitespace = ' ';

export type UrlString = string & {readonly __brand: 'UrlString'};

These types define the structures and constraints for various data types that the module can identify and work with. The use of TypeScript enhances type safety and ensures that the functions operate on expected data structures.

Utility Functions

The utilities.ts file provides utility functions that assist in the type-checking process:

export function keysOf<T extends Record<PropertyKey, unknown>>(value: T): Array<keyof T> {
    return Object.keys(value) as Array<keyof T>;
}

The keysOf function is a type-safe way to retrieve the keys of an object, ensuring that the returned keys are correctly typed.

Key Subsystems

Type Detection Logic

The core functionality of the module resides in index.ts, where the type detection logic is implemented. The detect function is central to this logic:

function detect(value: unknown): TypeName {
    if (value === null) {
        return 'null';
    }

    switch (typeof value) {
        case 'undefined': {
            return 'undefined';
        }
        case 'string': {
            return 'string';
        }
        case 'number': {
            return Number.isNaN(value) ? 'NaN' : 'number';
        }
        case 'boolean': {
            return 'boolean';
        }
        case 'function': {
            return 'Function';
        }
        case 'bigint': {
            return 'bigint';
        }
        case 'symbol': {
            return 'symbol';
        }
        default:
    }

    if (isObservable(value)) {
        return 'Observable';
    }

    if (isArray(value)) {
        return 'Array';
    }

    if (isBuffer(value)) {
        return 'Buffer';
    }

    const tagType = getObjectType(value);
    if (tagType && tagType !== 'Object') {
        return tagType;
    }

    if (hasPromiseApi(value)) {
        return 'Promise';
    }

    if (value instanceof String || value instanceof Boolean || value instanceof Number) {
        throw new TypeError('Please don\'t use object wrappers for primitive types');
    }

    return 'Object';
}

This function determines the type of a given value by checking against known JavaScript types and constructs. It uses a combination of typeof checks, instanceof checks, and custom logic to identify complex types like Observables and Promises.

Assertion Functions

The module also provides a set of assertion functions that throw errors if a value does not match the expected type. These are used to enforce type constraints in a programmatic way:

export function assertArray<T = unknown>(value: unknown, assertion?: (element: unknown, message?: string) => asserts element is T, message?: string): asserts value is T[] {
    if (!isArray(value)) {
        throw new TypeError(message ?? typeErrorMessage('Array', value));
    }

    if (assertion) {
        for (const element of value) {
            assertion(element, message);
        }
    }
}

The assertArray function checks if a value is an array and optionally applies an additional assertion to each element of the array.

Important Code Paths and Algorithms

Predicate Evaluation

The module includes functions for evaluating predicates over collections of values. These are implemented in the isAll and isAny functions:

export function isAll(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean | Predicate {
    if (Array.isArray(predicate)) {
        const predicateArray = predicate as readonly Predicate[];
        validatePredicateArray(predicateArray, values.length === 0);

        const combinedPredicate = (value: unknown) => predicateArray.every(singlePredicate => singlePredicate(value));
        if (values.length === 0) {
            return combinedPredicate;
        }

        return predicateOnArray(Array.prototype.every, combinedPredicate, values);
    }

    return predicateOnArray(Array.prototype.every, predicate as Predicate, values);
}

export function isAny(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean | Predicate {
    if (Array.isArray(predicate)) {
        const predicateArray = predicate as readonly Predicate[];
        validatePredicateArray(predicateArray, values.length === 0);

        const combinedPredicate = (value: unknown) => predicateArray.some(singlePredicate => singlePredicate(value));
        if (values.length === 0) {
            return combinedPredicate;
        }

        return predicateOnArray(Array.prototype.some, combinedPredicate, values);
    }

    return predicateOnArray(Array.prototype.some, predicate as Predicate, values);
}

These functions evaluate whether all or any of the provided predicates return true for the given values. They support both single predicates and arrays of predicates.

Error Handling

The module uses TypeScript's assertion functions extensively to handle errors. These functions throw TypeError instances with descriptive messages when an assertion fails:

function typeErrorMessage(description: AssertionTypeDescription, value: unknown): string {
    return `Expected value which is \`${description}\`, received value of type \`${is(value)}\`.`;
}

The typeErrorMessage function generates error messages that describe the expected type and the actual type received, aiding in debugging.

Extension Points

The sindresorhus/is module is designed to be extensible. New type-checking functions can be added by following the existing patterns in index.ts. Each function typically involves:

  1. Defining a TypeScript type or interface in types.ts if necessary.
  2. Implementing the type-checking logic in index.ts.
  3. Optionally, adding utility functions in utilities.ts to support the new functionality.

By adhering to the existing structure and conventions, developers can extend the module to include additional type checks as needed.

Press Enter to start a conversation