radashi

repository·main·Indexed 21 days ago

https://github.com/radashi-org/radashi

A modern, community-first TypeScript utility toolkit designed as a high-performance, type-safe, and tree-shakeable alternative to Lodash. It is a dependency-free library focusing on modern ES6+ syntax and robust type definitions.

Tokens
73.3K
Snippets
292
Records
318
Agent score
75%

What's inside radashi

  1. Overview of radashi-db databases

    main

    The radashi-db project manages two primary databases used to power the Radashi website and VSCode extension:

    1. Supabase: Used for website data and continuous benchmarking to detect performance regressions.
    2. Algolia: Used for website data and search functionality.

    Important Note on Types: The file supabase.types.ts is automatically generated from the Supabase schema. Do not manually edit this file, as changes will be overwritten during the generation process.

  2. Core Radashi utility functions

    main

    When writing TypeScript or JavaScript code, use these core utility functions from the radashi package. These functions are designed to simplify common tasks like type narrowing, array manipulation, and object transformations.

    Type Guarding and Assertions

    • assert: Asserts a condition and narrows types; throws an error if the condition is false.
    • isDate: Tests if a value is a Date.
    • isError: Tests if a value is an Error.
    • isNullish: Tests if a value is null or undefined.
    • isObject: Tests if a value is object-like.
    • isPlainObject: Tests if a value is an ordinary JSON-style object.
    • getErrorMessage: Converts an unknown caught error into a string message.

    Array Manipulation

    • castArray: Normalizes a value or an array into an array. Note: null becomes [null] and undefined becomes [undefined].
    • concat: Combines arrays or items. It drops null/undefined values and flattens one level of array nesting.
    • group: Groups array items by a specified key.
    • range: Creates an inclusive numeric generator.
    • sort: Returns a sorted copy of an array using a numeric getter.
    • sum: Calculates the sum of numbers or numeric fields within an array.
    • unique: Removes duplicate items from an array, with an optional key for comparison.

    Object Manipulation

    • mapValues: Transforms object values while preserving the original keys.
    • objectify: Converts an array into an object using a key. If duplicate keys exist, later values overwrite earlier ones.
    • omit: Returns a new object excluding the specified keys.
    • pick: Returns a new object containing only the specified keys (prefer using an array of keys).
    • shake: Removes only undefined properties from an object.

    Other Utilities

    • clamp: Constrains a number between a minimum and maximum value.
    • dedent: Formats multiline template strings to be more readable by removing indentation.
    • escapeHTML: Escapes HTML text characters (Note: this is not a full HTML sanitizer).
    • sleep: Delays execution of asynchronous code for a specified number of milliseconds.
    import { assert, castArray, isNullish, pick } from 'radashi';
    
    // Example usage
    assert(value !== null);
    const items = castArray(input);
    const subset = pick(user, ['id', 'name']);
  3. Migrating from Radash to Radashi

    main
    Radashi is an actively maintained fork of Radash. To ensure a smooth transition and maintain backward compatibility, it is recommended to install radashi@^12 if you are coming from Radash. This version will continue to receive fixes even after Radashi v13 is released, allowing you to upgrade to the latest version when you are ready.
  4. Use leave callbacks to run code after visiting children

    main

    If your visitor callback returns a function, that returned function acts as a "leave callback". It will be executed once traverse has finished visiting every property/element within the current object/array. A leave callback can also return false to exit the entire traversal early.

    import * as _ from 'radashi'
    // @noErrors
    
    _.traverse({ arr: ['a', 'b'] }, (value, key) => {
      if (isArray(value)) {
        console.log('start of array')
        return () => {
          console.log('end of array')
          return false
        }
      } else {
        console.log(key, '=>', value)
      }
    })
    // Logs:
    //     start of array
    //     0 => 'a'
    //     1 => 'b'
    //     end of array
  5. When to use `always` vs a standard arrow function

    main

    While you can often use a standard arrow function like () => true for primitives, always provides specific advantages for object references and memoization:

    1. Object Reference Stability: () => ({ a: 1 }) creates a new object on every call. _.always({ a: 1 }) returns the same object reference every time.
    2. Memoization: Instead of calling a heavy function every time () => someCalculation() is invoked, you can use _.always(someCalculation()) to return the pre-calculated result immediately on subsequent calls.
    import * as _ from 'radashi'
    
    // Not memoized: runs calculation every time
    const fn1 = () => someCalculation()
    
    // Memoized: returns the result of the calculation
    const fn2 = _.always(someCalculation())
    
    // Not same object: returns a new object every time
    const fn3 = () => ({ a: 1, b: 2 })
    
    // Same object: returns the same reference every time
    const fn4 = _.always({ a: 1, b: 2 })
  6. Understand the mental model of function composition in Radashi

    main

    In Radashi, compose uses a pattern where each function in the composition is a higher-order function that accepts the 'next' function in the chain.

    If you have a composition: _.compose(f, g, h)

    It is functionally equivalent to nesting the calls: f(g(h(...)))

    This means the last function in the compose arguments list is the innermost function that actually receives the initial data/execution trigger, and the first function in the list is the outermost wrapper.

    // Equivalent to _.compose(useZero, objectize, increment, increment, returnArg('num'))
    const decomposed = useZero(objectize(increment(increment(_.returnArg('num')))))
    
    decomposed() // => 2
  7. TypeScript narrowing with isObject

    main

    The isObject function uses the type predicate value is object.

    Caution: Because TypeScript's object type includes arrays, functions, dates, maps, and sets, isObject does not narrow a value specifically to a "plain object". It only narrows the value to the general object type.

    Best Practices:

    1. Use for Options/Primitives: It is effective when you need to distinguish between an options object and a primitive shorthand (like a string).
    2. Avoid for Complex Unions: If your union type includes arrays, functions, or other special objects, do not rely solely on isObject. Instead, narrow those specific cases using _.isArray or _.isFunction to ensure accurate type narrowing.
    import * as _ from 'radashi'
    
    type Options = { foo?: string }
    
    // Pattern 1: Handling Options vs Primitive
    declare let value: Options | string | undefined
    
    const options: Options = _.isObject(value)
      ? value
      : value === undefined
        ? {}
        : { foo: value }
    
    // Pattern 2: Handling complex unions with specific checks
    declare const value: Options | (() => Options) | Options[]
    
    if (_.isArray(value)) {
      value
      // ^? const value: Options[]
    } else if (_.isFunction(value)) {
      value
      // ^? const value: () => Options
    } else if (_.isObject(value)) {
      value
      // ^? const value: Options
    }
  8. Handle errors in parallel() with AggregateError

    main

    If any errors occur during the processing of the array, parallel will collect them. Once the entire array has been processed, it throws an AggregateError.

    To handle these errors gracefully without a try/catch block, you can wrap the call in _.tryit(). The resulting AggregateError contains an errors property, which is an array of all individual errors thrown during execution.

    // @noErrors
    import * as _ from 'radashi'
    
    const userIds = [1, 2, 3]
    
    const [err, users] = await _.tryit(_.parallel)(3, userIds, async userId => {
      throw new Error(`No, I don't want to find user ${userId}`)
    })
    
    console.log(err) // => AggregateError
    console.log(err.errors) // => [Error, Error, Error]
    console.log(err.errors[1].message) // => "No, I don't want to find user 2"
  9. Type safety warning when using predicate functions with pick()

    main

    When using a predicate function with pick, be aware of potential type inaccuracies due to TypeScript's partial type matching.

    If the object passed at runtime contains properties that are not defined in its TypeScript interface, the key and value parameters in your callback will still reflect the types defined in the interface. This means the TypeScript compiler might believe certain branches of logic (like an else block for unexpected keys) are unreachable, even though they may execute at runtime.

    // Example demonstrating potential inaccuracy in `key` and `value` types within `_.pick` callback
    import * as _ from 'radashi'
    // @noErrors
    
    interface User {
      name: string
      age: number
    }
    
    function getUserDetails(user: User) {
      return _.pick(user, (value, key) => {
        // TypeScript believes `key` is 'name' | 'age', but at runtime
        // it could be 'email'
        if (key === 'name' || key === 'age') {
          console.log(key, '=', value)
        } else {
          // TypeScript believes this will never run, but it does.
          console.log('Unexpected key:', key)
        }
      })
    }
    
    // At runtime, the function may receive an object with more properties
    const runtimeUser = {
      name: 'John',
      age: 30,
      // This property is not listed in the User type:
      email: 'john@example.com',
    }
    
    getUserDetails(runtimeUser)
    // Logs the following:
    //     name = John
    //     age = 30
    //     Unexpected key: email
  10. Optimize cloning performance with FastCloningStrategy

    main

    For better performance, you can pass FastCloningStrategy to cloneDeep.

    Tradeoffs:

    • All plain objects and class instances are cloned using the spread operator {...obj}.
    • Loss of data: The original prototype, computed properties, and non-enumerable properties are not preserved.
    • Limitations: Built-in complex objects like RegExp and Date are still not cloned with this strategy. To handle these, you must override the cloneOther function in your strategy.
  11. Understand the `TraverseContext` object

    main

    During each visit, a TraverseContext object is provided to the visitor. It contains metadata about the current traversal state:

    • key: The current key being visited.
    • parent: The parent object of the current value.
    • parents: An array of objects (from parent to child) that the current value is contained by.
    • path: An array describing the key path to the current value from the root.
    • skip: A function used to prevent traversal of an object. Calling skip() with no arguments skips the current value. Calling skip(obj) skips a specific object.
    • skipped: A set of objects that have been skipped.
    • value: The current value being visited.

    ⚠️ Warning: The path and parents arrays are mutated by the traverse function. If you need to use them outside the current visit, you must create a copy.