npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pechynho/stimulus-typescript

v0.0.15

Published

Set of TypeScript utilities for Stimulus controllers

Readme

Stimulus TypeScript

This project is based on the following projects:

I would like to thank the authors of these projects for their work, which served as the foundation for this package.

MIT Licenses of Original Projects

Usage

This package provides strongly typed Stimulus controllers with TypeScript, offering type safety for values, targets, classes, outlets, and portals.

Basic Usage

import {Controller} from '@hotwired/stimulus';
import {Target, Typed, TypedArray, TypedObject} from '@pechynho/stimulus-typescript';
import {UserStatusController} from './user-status-controller';
import {CustomElement} from './custom-element';

class HomepageController extends Typed(
    Controller<HTMLElement>, {
        values: {
            name: String,
            counter: Number,
            isActive: Boolean,
            alias: TypedArray<string>(),
            address: TypedObject<{ street: string }>(),
        },
        targets: {
            form: HTMLFormElement,
            select: HTMLSelectElement,
            custom: Target<CustomElement>(),
        },
        classes: ['selected', 'highlighted'] as const,
        outlets: {'user-status': UserStatusController},
    }
)
{
    // All properties are now strongly typed!

    public connect(): void {
        // String values
        this.nameValue.split(' ');

        // Number values
        Math.floor(this.counterValue);

        // Boolean values
        this.isActiveValue;

        // Array values
        this.aliasValue.map(alias => alias.toUpperCase());

        // Object values
        console.log(this.addressValue.street);

        // Targets
        this.formTarget.submit();
        this.selectTarget.value = 'stimulus';
        this.customTarget.someCustomMethod();

        // Outlets
        this.userStatusOutlets.forEach(status => status.markAsSelected(event));

        // Classes
        if (this.hasSelectedClass) {
            console.log(this.selectedClass);
        }
    }
}

Type Definitions

Values

The values object defines the types of values that can be set on your controller:

import {TypedArray, TypedObject} from "./typed-stimulus";

const values = {
    // Basic types
    name: String, // string
    count: Number, // number
    isActive: Boolean, // boolean

    // Array types
    tags: TypedArray<string>(), // string[]
    scores: TypedArray<number>(), // number[]

    // Custom object type
    user: TypedObject<{
        firstName: string,
        lastName: string,
        age: number
    }>()
};

Targets

The targets object defines the HTML elements that your controller can target:

import {Target} from '@pechynho/stimulus-typescript';
import {CustomElement} from './custom-element';

const targets = {
  form: HTMLFormElement, // <div data-homepage-controller-target="form"></div>
  button: HTMLButtonElement, // <button data-homepage-controller-targe="bubton"></button>
  input: HTMLInputElement, // <input data-homepage-controller-target="input">
  custom: Target<CustomElement>(), // <div data-homepage-controller-target="custom"></div>
}

Classes

The classes array defines CSS classes that your controller can add/remove:

const classes = ['selected', 'highlighted', 'active'] as const;

// Usage:
this.hasSelectedClass // boolean
this.selectedClass // string (class name)

Outlets

The outlets object defines other controllers that your controller can communicate with:

import {UserStatusController} from './user-status-controller';
import {NotificationController} from './notification-controller';

const outlets = {
  'user-status': UserStatusController,
  'notification': NotificationController
}

// Usage:
this.hasUserStatusOutlet // boolean
this.userStatusOutlet // UserStatusController
this.userStatusOutlets // UserStatusController[]

Portals

When you define portals in your controller, the system:

  1. Monitors these elements for targets and actions
  2. Makes these targets available to your controller
  3. Routes actions from these elements to your controller

This is especially useful for modals, sidebars, or any other elements that might be rendered outside your controller's DOM tree but still need to interact with your controller.

You need to register special PortalController to your Stimulus application:

import { Application } from '@hotwired/stimulus';
import { PortalController } from '@pechynho/stimulus-typescript';

const app = Application.start(); // Start your Stimulus application

app.register('portal', PortalController); // Register PortalController

Example

import { Controller } from '@hotwired/stimulus';
import { Typed, Portals } from '@pechynho/stimulus-typescript';

class ModalController extends Typed(
    Portals(Controller<HTMLElement>), {
        targets: {
            content: HTMLDivElement
        },
    }
) {
  public open(): void {
    // Even if #modal is outside this controller's DOM,
    // you can still access targets inside it
    this.contentTarget.classList.add('visible');
  }
  
  public close(): void {
    this.contentTarget.classList.remove('visible');
  }
}

In your HTML:

<div data-controller="modal" data-modal-portal-selectors-value="[#modal]">
  <button data-action="modal#open">Open Modal</button>
</div>

<!-- This is outside the controller's DOM -->
<div id="modal">
  <div data-modal-target="content">
    Modal content here
    <button data-action="modal#close">Close</button>
  </div>
</div>

With portals, the ModalController can interact with elements inside #modal even though they're outside its DOM hierarchy.

Resolvable

When you use the Resolvable feature, your controller class gains two static methods:

  1. get<T>: Synchronously gets a controller instance for a specific element
  2. getAsync<T>: Asynchronously gets a controller instance with timeout and polling options

Example

import { Controller } from '@hotwired/stimulus';
import { Typed, Resolvable } from '@pechynho/stimulus-typescript';

class UserController extends Typed(
    Resolvable(Controller<HTMLElement>, 'user'), {
        values: {
            name: String,
        },
    }
) {
    public greet(): void {
        console.log(`Hello, ${this.nameValue}!`);
    }
}

// Later, in another part of your code:
const userElement = document.querySelector('#user');

// Synchronous access (returns null if controller is not found)
const userController = UserController.get(userElement);
if (userController) {
    userController.greet();
}

// Asynchronous access (resolves when controller is found or rejects after timeout)
UserController.getAsync(userElement)
    .then(controller => {
        if (controller !== null) {
            controller.greet();
        }
    })
    .catch(error => console.error(error));

// With custom timeout and polling interval (in milliseconds)
UserController.getAsync(userElement, 10000, 100)
    .then(controller => {
        if (controller !== nu) {
            controller.greet();
        }
    })
    .catch(error => console.error(error));

This is particularly useful when:

  • Working with dynamically loaded content
  • Integrating with non-Stimulus JavaScript libraries
  • Communicating between controllers that don't have a parent-child relationship
  • You've just added an element to the DOM and want it to resolve to a controller, so you use getAsync and you do not have to deal with Stimulus internal timing (has Stimulus already discovered a new element and connected controller?)