Typedora UI
Components

TypedSelect

A headless, type-safe select component with full TypeScript inference and customizable rendering.

Introduction

TypedSelect is a headless, type-safe wrapper around the base Select component. It provides full TypeScript inference for option values while giving you complete control over rendering through render props.

Key features:

  • Full type-safety for string, number, and boolean values
  • Headless design with render props for complete customization
  • Support for grouped options
  • Controlled and uncontrolled usage

Installation

npx shadcn@latest add https://typedora-ui.netlify.app/r/typed-select.json
pnpm dlx shadcn@latest add https://typedora-ui.netlify.app/r/typed-select.json
npx shadcn@latest add https://typedora-ui.netlify.app/r/typed-select.json
bunx --bun shadcn@latest add https://typedora-ui.netlify.app/r/typed-select.json

Usage

Basic Usage

basic-usage.tsx
import {  } from "@/components/typed-select";

export function () {
  return (
    <>
      <
        ={
          [
            { : "Apple", : "apple" },
            { : "Banana", : "banana" },
            { : "Orange", : "orange" },
          ] as 
        }
        ={(value) => {
value: "apple" | "banana" | "orange"
}} /> </> ); }

Number Values

number-values.tsx
import {  } from "@/components/typed-select";

export function () {
  return (
    <>
      <
        ={
          [
            { : "One", : 1 },
            { : "Two", : 2 },
            { : "Three", : 3 },
          ] as 
        }
        ={(value) => {
value: 1 | 2 | 3
}} /> </> ); }

Boolean Values

boolean-values.tsx
import {  } from "@/components/typed-select";

export function () {
  return (
    <>
      <
        ={[
          { : "Yes", : true },
          { : "No", : false },
        ]}
        ={(value) => {
value: boolean
}} /> </> ); }

Custom Item rendering

custom-item.tsx
import {  } from "@/components/typed-select";

export function () {
  return (
    <>
      <
        ="Select a plan"
        ={[
          { : "Free", : "free", : "For personal use" },
          { : "Pro", : "pro", : "For professionals" },
          { : "Team", : "team", : "For small teams" },
        ]}
        ={({ , ,  }) => (
          < {...} ="flex flex-col items-start py-2">
            < ="font-medium">{.}</>
            < ="text-sm text-muted-foreground">
              {.}
            </>
          </>
        )}
      />
    </>
  );
}

Grouped Options

grouped-options.tsx
import {  } from "@/components/typed-select";

export function () {
  return (
    <>
      <
        ={
          [
            {
              : "Fruits",
              : [
                { : "Apple", : "apple" },
                { : "Banana", : "banana" },
              ],
            },
            {
              : "Vegetables",
              : [
                { : "Carrot", : "carrot" },
                { : "Broccoli", : "broccoli" },
              ],
            },
          ] as 
        }
        ={(value) => {
value: "apple" | "banana" | "carrot" | "broccoli"
}} /> </> ); }

Disabled Options

disabled-options.tsx
import {  } from "@/components/typed-select";

export function () {
  return (
    <>
      <
        ={[
          { : "Apple", : "apple" },
          { : "Banana", : "banana", : true },
          { : "Orange", : "orange" },
        ]}
      />
    </>
  );
}

Controlled Component

controlled.tsx
"use client";

import {  } from "react";
import {  } from "@/components/typed-select";

type  = {
  : string;
  : string;
};

const : [] = [
  { : "1", : "Alice" },
  { : "2", : "Bob" },
  { : "3", : "Charlie" },
];

export function () {
  const [, ] = <["id"]>([0].);

  return (
    < ="space-y-4">
      <
        ={}
        ={}
        ="id"
        ="name"
        ={}
      />
      <>Selected: {}</>
    </>
  );
}

Selected: 1

With Search Filter

search-filter.tsx
"use client";

import {  } from "react";
import {  } from "@/components/typed-select";
import {  } from "@/components/ui/input";

export function () {
  const [, ] = ("");

  const  = [
    { : "Apple", : "apple" },
    { : "Banana", : "banana" },
    { : "Cherry", : "cherry" },
  ];

  const  = .(() =>
    ..().(.()),
  );

  return (
    <
      ={}
      ={({ ,  }) => (
        <
          // Prevent Select from closing when clicking inside the search input
          ={() => {
            const  = . as HTMLElement;
            if (.("input")) {
              .();
            }
          }}
        >
          < ="p-2 border-b" ={() => .()}>
            <
              ="Search..."
              ={}
              ={() => (..)}
              // Prevent focus management interference from Radix
              ={() => .()}
            />
          </>
          {}
        </>
      )}
    />
  );
}

Custom Keys (id/name)

By default, TypedSelect uses value and label keys. You can customize this using valueKey and labelKey props:

custom-keys.tsx
"use client";

import {  } from "react";
import {  } from "@/components/typed-select";

// Example data using id/name keys instead of value/label

type  = {
  : number;
  : string;
  : string;
};

const : [] = [
  { : 1, : "John Doe", : "john@example.com" },
  { : 2, : "Jane Smith", : "jane@example.com" },
  { : 3, : "Bob Johnson", : "bob@example.com" },
];

export function () {
  const [, ] = <["id"]>();

  return (
    < ="space-y-4">
      <
        ="Select a user"
        ={}
        ="id"
        ="name"
        ={}
        ={}
        ={({ , ,  }) => (
          < {...} ="flex flex-col items-start py-2">
            < ="font-medium">{.}</>
            < ="text-sm text-muted-foreground">
              {.}
            </>
          </>
        )}
      />
      { && (
        < ="text-sm text-muted-foreground">
          Selected user ID: <>{}</>
        </>
      )}
    </>
  );
}

API Reference

TypedSelectProps

PropType
optionsarray
valueKey?TValueKey
labelKey?TLabelKey
value?ExtractOptionValue<TOption, TValueKey>
defaultValue?ExtractOptionValue<TOption, TValueKey>
onValueChange?function
placeholder?string
renderTrigger?function
renderItem?function
renderGroup?function
renderContent?function
disabled?boolean
open?boolean
onOpenChange?function
name?string
required?boolean

SelectOption

PropType
valueT
labelstring
disabled?boolean

SelectOptionGroup

PropType
labelstring
optionsarray

SelectTriggerRenderProps

PropType
selectedOptionTOption | undefined
placeholderstring
openboolean
disabledboolean
Triggerfunction
Valuefunction

SelectItemRenderProps

PropType
optionTOption
isSelectedboolean
isDisabledboolean
groupSelectOptionGroup<TOption> | undefined
itemPropsobject
Itemfunction

SelectGroupRenderProps

PropType
groupSelectOptionGroup<TOption>
childrenReactNode
Groupfunction
Labelfunction

SelectContentRenderProps

PropType
childrenReactNode
Contentfunction

On this page