+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 182 of 354

๐Ÿ“˜ TypeScript with Svelte: Compile-Time Types

Master typescript with svelte: compile-time types in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write type-safe code โœจ

๐ŸŽฏ Introduction

Welcome to the exciting world of TypeScript with Svelte! ๐ŸŽ‰ In this comprehensive guide, weโ€™ll explore how Svelteโ€™s compile-time magic works perfectly with TypeScriptโ€™s type safety to create lightning-fast, type-safe applications.

Youโ€™ll discover how TypeScript and Svelte combine to give you the best of both worlds: Svelteโ€™s incredible performance and TypeScriptโ€™s bulletproof type safety. Whether youโ€™re building web applications ๐ŸŒ, interactive dashboards ๐Ÿ“Š, or component libraries ๐Ÿ“š, understanding TypeScript with Svelte is essential for modern web development.

By the end of this tutorial, youโ€™ll feel confident using TypeScript with Svelte in your own projects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding TypeScript with Svelte

๐Ÿค” What is TypeScript with Svelte?

TypeScript with Svelte is like having a super-powered development experience ๐Ÿš€. Think of it as having a personal assistant that checks your code while Svelteโ€™s compiler works its magic to create incredibly fast applications.

In TypeScript terms, Svelte provides compile-time optimization while TypeScript provides compile-time type checking. This means you can:

  • โœจ Catch errors before they reach your users
  • ๐Ÿš€ Get amazing performance from Svelteโ€™s compiler
  • ๐Ÿ›ก๏ธ Enjoy type safety throughout your components
  • ๐Ÿ’ก Benefit from excellent IDE support and autocompletion

๐Ÿ’ก Why Use TypeScript with Svelte?

Hereโ€™s why developers love this combination:

  1. Compile-Time Safety ๐Ÿ”’: Both TypeScript and Svelte catch issues at build time
  2. Amazing Performance โšก: Svelte generates vanilla JavaScript with minimal runtime overhead
  3. Developer Experience ๐Ÿ’ป: Incredible IDE support and debugging capabilities
  4. Smaller Bundle Sizes ๐Ÿ“ฆ: Svelteโ€™s compiler removes unused code automatically

Real-world example: Imagine building a real-time chat application ๐Ÿ’ฌ. With TypeScript and Svelte, you get type-safe message handling with incredible performance!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Setting Up TypeScript with Svelte

Letโ€™s start with setting up your Svelte project with TypeScript:

# ๐ŸŽฏ Create a new Svelte project with TypeScript
npm create svelte@latest my-svelte-app
cd my-svelte-app

# ๐Ÿ“ฆ Choose TypeScript template during setup
npm install

๐ŸŽจ Your First TypeScript Svelte Component

Hereโ€™s a simple TypeScript-powered Svelte component:

<!-- ๐Ÿ“˜ HelloWorld.svelte -->
<script lang="ts">
  // ๐Ÿ‘‹ TypeScript in Svelte!
  export let name: string = "World"; // ๐ŸŽฏ Typed prop with default
  export let count: number = 0;      // ๐Ÿ”ข Number prop
  
  // ๐ŸŽจ Typed reactive statement
  $: greeting = `Hello, ${name}! ๐Ÿ‘‹`;
  $: isHighCount = count > 5; // ๐Ÿ”ฅ Boolean derived from count
  
  // ๐ŸŽฎ Typed event handler
  const handleClick = (): void => {
    count += 1;
    console.log(`Clicked! Count is now: ${count} ๐ŸŽ‰`);
  };
  
  // ๐ŸŽจ Interface for complex data
  interface UserData {
    id: number;
    name: string;
    emoji: string;
  }
  
  const user: UserData = {
    id: 1,
    name: "TypeScript Fan",
    emoji: "๐Ÿš€"
  };
</script>

<div class="container">
  <h1>{greeting}</h1>
  <p>User: {user.emoji} {user.name}</p>
  
  <button 
    on:click={handleClick}
    class:active={isHighCount}
  >
    Clicked {count} times! ๐ŸŽฏ
  </button>
  
  {#if isHighCount}
    <p>๐Ÿ”ฅ You're on fire! Great clicking!</p>
  {/if}
</div>

<style>
  .container {
    text-align: center;
    padding: 2rem;
  }
  
  .active {
    background-color: #ff6b6b;
    color: white;
  }
</style>

๐Ÿ’ก Explanation: Notice how we use lang="ts" in the script tag! This tells Svelte to use TypeScript for type checking.

๐ŸŽฏ Common TypeScript Patterns in Svelte

Here are patterns youโ€™ll use daily:

<!-- ๐Ÿ—๏ธ Advanced Component with Types -->
<script lang="ts">
  // ๐ŸŽจ Union types for component states
  type LoadingState = "idle" | "loading" | "success" | "error";
  type Theme = "light" | "dark" | "auto";
  
  // ๐Ÿ“‹ Props with complex types
  export let items: Array<{id: string; name: string; emoji: string}> = [];
  export let theme: Theme = "light";
  export let onItemClick: (id: string) => void = () => {};
  
  let loadingState: LoadingState = "idle";
  
  // ๐Ÿ”„ Async function with proper typing
  const loadData = async (): Promise<void> => {
    loadingState = "loading";
    
    try {
      // ๐ŸŽฏ Simulated API call
      await new Promise(resolve => setTimeout(resolve, 1000));
      loadingState = "success";
    } catch (error) {
      loadingState = "error";
      console.error("Failed to load data:", error);
    }
  };
</script>

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: TypeScript Shopping Cart Component

Letโ€™s build a real shopping cart with TypeScript:

<!-- ๐Ÿ›๏ธ ShoppingCart.svelte -->
<script lang="ts">
  // ๐Ÿ›’ Product interface
  interface Product {
    id: string;
    name: string;
    price: number;
    emoji: string;
    category: "electronics" | "clothing" | "books" | "food";
  }
  
  // ๐Ÿงพ Cart item extends product with quantity
  interface CartItem extends Product {
    quantity: number;
  }
  
  // ๐Ÿ“ฆ Props
  export let products: Product[] = [];
  
  // ๐ŸŽฏ Component state
  let cart: CartItem[] = [];
  let isCheckingOut: boolean = false;
  
  // ๐Ÿ’ฐ Computed total with type safety
  $: total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  $: itemCount = cart.reduce((sum, item) => sum + item.quantity, 0);
  
  // โž• Add to cart function
  const addToCart = (product: Product): void => {
    const existingItem = cart.find(item => item.id === product.id);
    
    if (existingItem) {
      // ๐Ÿ”„ Update existing item
      cart = cart.map(item => 
        item.id === product.id 
          ? { ...item, quantity: item.quantity + 1 }
          : item
      );
    } else {
      // ๐Ÿ†• Add new item
      cart = [...cart, { ...product, quantity: 1 }];
    }
    
    console.log(`Added ${product.emoji} ${product.name} to cart! ๐Ÿ›’`);
  };
  
  // โž– Remove from cart
  const removeFromCart = (productId: string): void => {
    cart = cart.filter(item => item.id !== productId);
  };
  
  // ๐Ÿ’ณ Checkout process
  const checkout = async (): Promise<void> => {
    isCheckingOut = true;
    
    try {
      // ๐ŸŽฏ Simulate payment processing
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      console.log(`๐ŸŽ‰ Order completed! Total: $${total.toFixed(2)}`);
      cart = []; // ๐Ÿงน Clear cart
      
    } catch (error) {
      console.error("Checkout failed:", error);
    } finally {
      isCheckingOut = false;
    }
  };
</script>

<div class="shopping-cart">
  <h2>๐Ÿ›’ TypeScript Shopping Cart</h2>
  
  <!-- ๐Ÿ“ฆ Products list -->
  <div class="products">
    <h3>๐Ÿ›๏ธ Available Products</h3>
    {#each products as product (product.id)}
      <div class="product-card">
        <span class="emoji">{product.emoji}</span>
        <span class="name">{product.name}</span>
        <span class="price">${product.price.toFixed(2)}</span>
        <button on:click={() => addToCart(product)}>
          Add to Cart โž•
        </button>
      </div>
    {/each}
  </div>
  
  <!-- ๐Ÿงพ Cart display -->
  <div class="cart">
    <h3>๐Ÿ›’ Your Cart ({itemCount} items)</h3>
    
    {#if cart.length === 0}
      <p>Your cart is empty ๐Ÿ˜ข</p>
    {:else}
      {#each cart as item (item.id)}
        <div class="cart-item">
          <span>{item.emoji} {item.name}</span>
          <span>Qty: {item.quantity}</span>
          <span>${(item.price * item.quantity).toFixed(2)}</span>
          <button on:click={() => removeFromCart(item.id)}>โŒ</button>
        </div>
      {/each}
      
      <div class="total">
        <strong>Total: ${total.toFixed(2)} ๐Ÿ’ฐ</strong>
      </div>
      
      <button 
        class="checkout-btn"
        on:click={checkout}
        disabled={isCheckingOut}
      >
        {isCheckingOut ? "Processing... โณ" : "Checkout ๐Ÿ’ณ"}
      </button>
    {/if}
  </div>
</div>

<style>
  .shopping-cart {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
  }
  
  .products, .cart {
    margin: 2rem 0;
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
  
  .product-card, .cart-item {
    display: flex;
    gap: 1rem;
    align-items: center;
    padding: 0.5rem;
    margin: 0.5rem 0;
    background: #f9f9f9;
    border-radius: 4px;
  }
  
  .checkout-btn {
    background: #28a745;
    color: white;
    padding: 0.75rem 1.5rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 1rem;
  }
  
  .checkout-btn:disabled {
    background: #6c757d;
    cursor: not-allowed;
  }
</style>

๐ŸŽฏ Try it yourself: Add a quantity selector and discount codes feature!

๐ŸŽฎ Example 2: Real-Time Game Score Dashboard

Letโ€™s create a live game dashboard:

<!-- ๐Ÿ† GameDashboard.svelte -->
<script lang="ts">
  // ๐ŸŽฎ Game interfaces
  interface Player {
    id: string;
    name: string;
    avatar: string; // emoji
    score: number;
    level: number;
    status: "online" | "offline" | "in-game";
  }
  
  interface GameMatch {
    id: string;
    players: Player[];
    startTime: Date;
    gameType: "quick" | "ranked" | "tournament";
    status: "waiting" | "active" | "finished";
  }
  
  // ๐Ÿ“Š Component state
  let players: Player[] = [
    { id: "1", name: "TypeScript Hero", avatar: "๐Ÿš€", score: 1500, level: 42, status: "online" },
    { id: "2", name: "Svelte Master", avatar: "โšก", score: 1200, level: 38, status: "in-game" },
    { id: "3", name: "Code Ninja", avatar: "๐Ÿฅท", score: 1800, level: 55, status: "online" }
  ];
  
  let currentMatch: GameMatch | null = null;
  let isMatchmaking: boolean = false;
  
  // ๐ŸŽฏ Reactive computations
  $: onlinePlayers = players.filter(p => p.status === "online");
  $: topPlayer = players.reduce((top, player) => 
    player.score > top.score ? player : top
  );
  $: averageLevel = Math.round(
    players.reduce((sum, p) => sum + p.level, 0) / players.length
  );
  
  // ๐ŸŽฎ Start matchmaking
  const startMatchmaking = async (): Promise<void> => {
    if (onlinePlayers.length < 2) {
      alert("Need at least 2 players online! ๐Ÿ‘ฅ");
      return;
    }
    
    isMatchmaking = true;
    
    try {
      // ๐Ÿ” Simulate matchmaking
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      currentMatch = {
        id: `match-${Date.now()}`,
        players: onlinePlayers.slice(0, 2), // Take first 2 players
        startTime: new Date(),
        gameType: "quick",
        status: "active"
      };
      
      // ๐ŸŽฏ Update player status
      players = players.map(player => 
        currentMatch!.players.some(p => p.id === player.id)
          ? { ...player, status: "in-game" }
          : player
      );
      
      console.log("๐ŸŽฎ Match started!", currentMatch);
      
    } catch (error) {
      console.error("Matchmaking failed:", error);
    } finally {
      isMatchmaking = false;
    }
  };
  
  // ๐Ÿ End match
  const endMatch = (): void => {
    if (!currentMatch) return;
    
    // ๐ŸŽ‰ Award points to winner (random for demo)
    const winner = currentMatch.players[Math.floor(Math.random() * currentMatch.players.length)];
    
    players = players.map(player => {
      if (player.id === winner.id) {
        return {
          ...player,
          score: player.score + 100,
          level: player.level + 1,
          status: "online"
        };
      }
      return currentMatch!.players.some(p => p.id === player.id)
        ? { ...player, status: "online" }
        : player;
    });
    
    console.log(`๐Ÿ† ${winner.avatar} ${winner.name} won the match!`);
    currentMatch = null;
  };
</script>

<div class="dashboard">
  <h1>๐ŸŽฎ Real-Time Game Dashboard</h1>
  
  <!-- ๐Ÿ“Š Stats overview -->
  <div class="stats">
    <div class="stat-card">
      <h3>๐Ÿ‘ฅ Online Players</h3>
      <p>{onlinePlayers.length}</p>
    </div>
    
    <div class="stat-card">
      <h3>๐Ÿ† Top Player</h3>
      <p>{topPlayer.avatar} {topPlayer.name}</p>
      <small>{topPlayer.score} points</small>
    </div>
    
    <div class="stat-card">
      <h3>๐Ÿ“ˆ Avg Level</h3>
      <p>{averageLevel}</p>
    </div>
  </div>
  
  <!-- ๐Ÿ‘ฅ Players list -->
  <div class="players">
    <h2>๐ŸŽฏ Players</h2>
    {#each players as player (player.id)}
      <div class="player-card" class:in-game={player.status === "in-game"}>
        <span class="avatar">{player.avatar}</span>
        <div class="player-info">
          <h4>{player.name}</h4>
          <p>Level {player.level} โ€ข {player.score} points</p>
          <span class="status {player.status}">{player.status}</span>
        </div>
      </div>
    {/each}
  </div>
  
  <!-- ๐ŸŽฎ Match controls -->
  <div class="match-controls">
    {#if currentMatch}
      <div class="current-match">
        <h3>๐ŸŽฏ Current Match</h3>
        <p>Game Type: {currentMatch.gameType}</p>
        <p>Players: {currentMatch.players.map(p => `${p.avatar} ${p.name}`).join(" vs ")}</p>
        <button on:click={endMatch}>End Match ๐Ÿ</button>
      </div>
    {:else}
      <button 
        on:click={startMatchmaking}
        disabled={isMatchmaking || onlinePlayers.length < 2}
      >
        {isMatchmaking ? "Finding Match... ๐Ÿ”" : "Start Matchmaking ๐ŸŽฎ"}
      </button>
    {/if}
  </div>
</div>

<style>
  .dashboard {
    max-width: 1000px;
    margin: 0 auto;
    padding: 2rem;
  }
  
  .stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
    margin: 2rem 0;
  }
  
  .stat-card {
    background: #f8f9fa;
    padding: 1.5rem;
    border-radius: 8px;
    text-align: center;
  }
  
  .players {
    margin: 2rem 0;
  }
  
  .player-card {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1rem;
    margin: 0.5rem 0;
    background: white;
    border: 1px solid #ddd;
    border-radius: 8px;
    transition: all 0.3s ease;
  }
  
  .player-card.in-game {
    background: #fff3cd;
    border-color: #ffeaa7;
  }
  
  .avatar {
    font-size: 2rem;
  }
  
  .status {
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    font-size: 0.8rem;
    font-weight: bold;
  }
  
  .status.online { background: #d4edda; color: #155724; }
  .status.offline { background: #f8d7da; color: #721c24; }
  .status.in-game { background: #d1ecf1; color: #0c5460; }
  
  .match-controls {
    text-align: center;
    margin: 2rem 0;
  }
  
  .current-match {
    background: #e7f3ff;
    padding: 1.5rem;
    border-radius: 8px;
    margin: 1rem 0;
  }
</style>

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: TypeScript Stores in Svelte

When youโ€™re ready to level up, try Svelte stores with TypeScript:

// ๐Ÿช stores.ts - Type-safe Svelte stores
import { writable, derived, readable } from 'svelte/store';

// ๐ŸŽฏ User store with TypeScript
interface User {
  id: string;
  name: string;
  email: string;
  preferences: {
    theme: 'light' | 'dark';
    notifications: boolean;
  };
}

export const user = writable<User | null>(null);

// ๐ŸŽจ Theme store derived from user
export const theme = derived(
  user,
  ($user): 'light' | 'dark' => $user?.preferences.theme ?? 'light'
);

// โฐ Real-time clock store
export const currentTime = readable<Date>(new Date(), (set) => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  return () => clearInterval(interval);
});

// ๐Ÿ›’ Shopping cart store with complex types
interface CartState {
  items: Array<{
    id: string;
    name: string;
    price: number;
    quantity: number;
  }>;
  total: number;
  discount: number;
}

const createCartStore = () => {
  const { subscribe, set, update } = writable<CartState>({
    items: [],
    total: 0,
    discount: 0
  });

  return {
    subscribe,
    addItem: (item: Omit<CartState['items'][0], 'quantity'>) => {
      update(state => {
        const existingItem = state.items.find(i => i.id === item.id);
        
        if (existingItem) {
          existingItem.quantity += 1;
        } else {
          state.items.push({ ...item, quantity: 1 });
        }
        
        state.total = state.items.reduce((sum, i) => sum + (i.price * i.quantity), 0);
        return state;
      });
    },
    clear: () => set({ items: [], total: 0, discount: 0 })
  };
};

export const cart = createCartStore();

๐Ÿ—๏ธ Advanced Topic 2: Component Typing with Slots

For the brave developers, hereโ€™s advanced component typing:

<!-- ๐ŸŽจ AdvancedModal.svelte -->
<script lang="ts">
  import type { SvelteComponent } from 'svelte';
  
  // ๐ŸŽฏ Complex prop types
  interface ModalAction {
    label: string;
    variant: 'primary' | 'secondary' | 'danger';
    action: () => void;
    disabled?: boolean;
  }
  
  export let isOpen: boolean = false;
  export let title: string;
  export let size: 'small' | 'medium' | 'large' = 'medium';
  export let actions: ModalAction[] = [];
  export let onClose: () => void = () => {};
  export let allowBackdropClose: boolean = true;
  
  // ๐ŸŽช Slot props typing
  interface ModalSlotProps {
    closeModal: () => void;
    modalId: string;
  }
  
  const modalId = `modal-${Math.random().toString(36).substr(2, 9)}`;
  
  const closeModal = (): void => {
    isOpen = false;
    onClose();
  };
  
  const handleBackdropClick = (event: MouseEvent): void => {
    if (allowBackdropClose && event.target === event.currentTarget) {
      closeModal();
    }
  };
  
  // ๐ŸŽฏ Typed slot props
  const slotProps: ModalSlotProps = {
    closeModal,
    modalId
  };
</script>

{#if isOpen}
  <div 
    class="modal-backdrop" 
    on:click={handleBackdropClick}
    on:keydown={(e) => e.key === 'Escape' && closeModal()}
    role="dialog"
    aria-labelledby="{modalId}-title"
    aria-modal="true"
  >
    <div class="modal-content {size}">
      <header class="modal-header">
        <h2 id="{modalId}-title">{title}</h2>
        <button class="close-btn" on:click={closeModal}>โŒ</button>
      </header>
      
      <main class="modal-body">
        <!-- ๐ŸŽช Named slot with typed props -->
        <slot name="content" {closeModal} {modalId} />
        
        <!-- ๐ŸŽฏ Default slot with props -->
        <slot {...slotProps} />
      </main>
      
      {#if actions.length > 0}
        <footer class="modal-footer">
          {#each actions as action}
            <button
              class="action-btn {action.variant}"
              disabled={action.disabled}
              on:click={action.action}
            >
              {action.label}
            </button>
          {/each}
        </footer>
      {/if}
    </div>
  </div>
{/if}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Forgetting lang="ts" in Script Tags

<!-- โŒ Wrong way - no TypeScript! -->
<script>
  let name: string = "Hello"; // โš ๏ธ Type annotation ignored!
</script>

<!-- โœ… Correct way - TypeScript enabled! -->
<script lang="ts">
  let name: string = "Hello"; // โœ… Type safety enabled!
</script>

๐Ÿคฏ Pitfall 2: Incorrect Event Handler Typing

<!-- โŒ Dangerous - untyped events! -->
<script lang="ts">
  const handleClick = (event) => { // ๐Ÿ’ฅ `event` is `any`!
    console.log(event.target.value); // Might not exist!
  };
</script>

<!-- โœ… Safe - properly typed events! -->
<script lang="ts">
  const handleClick = (event: MouseEvent): void => {
    const target = event.target as HTMLButtonElement;
    console.log("Button clicked:", target.textContent); // โœ… Type safe!
  };
  
  const handleInput = (event: Event): void => {
    const target = event.target as HTMLInputElement;
    console.log("Input value:", target.value); // โœ… Safe access!
  };
</script>

๐Ÿ”ง Pitfall 3: Store Typing Issues

// โŒ Wrong way - losing type safety in stores!
import { writable } from 'svelte/store';

const userData = writable(null); // ๐Ÿ’ฅ Type is `any`!

// โœ… Correct way - explicit store typing!
import { writable } from 'svelte/store';

interface User {
  id: string;
  name: string;
}

const userData = writable<User | null>(null); // โœ… Typed store!

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Always Use lang="ts": Enable TypeScript in all your script tags
  2. ๐Ÿ“ Type Your Props: Explicitly type all component props and their defaults
  3. ๐Ÿ›ก๏ธ Type Event Handlers: Use proper event types for all event handlers
  4. ๐ŸŽจ Interface Everything: Create interfaces for complex data structures
  5. โœจ Leverage Reactive Statements: Use $: with proper TypeScript inference
  6. ๐Ÿช Type Your Stores: Always provide explicit types for Svelte stores
  7. ๐ŸŽช Type Slot Props: Define interfaces for slot prop types

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a TypeScript Todo App with Svelte

Create a fully type-safe todo application with these features:

๐Ÿ“‹ Requirements:

  • โœ… Todo items with title, completed status, priority, and due date
  • ๐Ÿท๏ธ Categories for todos (work, personal, shopping)
  • ๐Ÿ‘ค User assignment feature
  • ๐Ÿ“… Sorting and filtering capabilities
  • ๐ŸŽจ Each todo needs an emoji based on category!
  • ๐Ÿ’พ Local storage persistence

๐Ÿš€ Extra Credit:

  • Add drag-and-drop reordering
  • Implement todo search functionality
  • Create a statistics dashboard
  • Add keyboard shortcuts

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
<!-- ๐Ÿ“‹ TodoApp.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  
  // ๐ŸŽฏ Todo interfaces
  interface Todo {
    id: string;
    title: string;
    completed: boolean;
    priority: 'low' | 'medium' | 'high';
    category: 'work' | 'personal' | 'shopping';
    dueDate?: Date;
    assignee?: string;
    createdAt: Date;
  }
  
  type FilterType = 'all' | 'active' | 'completed';
  type SortType = 'created' | 'priority' | 'dueDate' | 'title';
  
  // ๐Ÿ“ฆ Component state
  let todos: Todo[] = [];
  let newTodoTitle: string = '';
  let selectedCategory: Todo['category'] = 'personal';
  let selectedPriority: Todo['priority'] = 'medium';
  let filter: FilterType = 'all';
  let sortBy: SortType = 'created';
  let searchTerm: string = '';
  
  // ๐ŸŽจ Category emojis
  const categoryEmojis: Record<Todo['category'], string> = {
    work: '๐Ÿ’ผ',
    personal: '๐Ÿ ',
    shopping: '๐Ÿ›’'
  };
  
  // ๐ŸŽฏ Priority colors
  const priorityColors: Record<Todo['priority'], string> = {
    low: '#28a745',
    medium: '#ffc107',
    high: '#dc3545'
  };
  
  // ๐Ÿ”„ Reactive filtering and sorting
  $: filteredTodos = todos
    .filter(todo => {
      // ๐ŸŽฏ Filter by completion status
      if (filter === 'active' && todo.completed) return false;
      if (filter === 'completed' && !todo.completed) return false;
      
      // ๐Ÿ” Filter by search term
      if (searchTerm && !todo.title.toLowerCase().includes(searchTerm.toLowerCase())) {
        return false;
      }
      
      return true;
    })
    .sort((a, b) => {
      switch (sortBy) {
        case 'priority':
          const priorityOrder = { high: 3, medium: 2, low: 1 };
          return priorityOrder[b.priority] - priorityOrder[a.priority];
        case 'dueDate':
          if (!a.dueDate && !b.dueDate) return 0;
          if (!a.dueDate) return 1;
          if (!b.dueDate) return -1;
          return a.dueDate.getTime() - b.dueDate.getTime();
        case 'title':
          return a.title.localeCompare(b.title);
        default:
          return b.createdAt.getTime() - a.createdAt.getTime();
      }
    });
  
  // ๐Ÿ“Š Statistics
  $: completedCount = todos.filter(t => t.completed).length;
  $: totalCount = todos.length;
  $: completionRate = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
  
  // โž• Add new todo
  const addTodo = (): void => {
    if (!newTodoTitle.trim()) return;
    
    const newTodo: Todo = {
      id: Date.now().toString(),
      title: newTodoTitle.trim(),
      completed: false,
      priority: selectedPriority,
      category: selectedCategory,
      createdAt: new Date()
    };
    
    todos = [newTodo, ...todos];
    newTodoTitle = '';
    saveTodos();
    
    console.log(`โœ… Added todo: ${categoryEmojis[newTodo.category]} ${newTodo.title}`);
  };
  
  // โœ… Toggle completion
  const toggleTodo = (id: string): void => {
    todos = todos.map(todo => 
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    );
    saveTodos();
  };
  
  // โŒ Delete todo
  const deleteTodo = (id: string): void => {
    const todo = todos.find(t => t.id === id);
    todos = todos.filter(t => t.id !== id);
    saveTodos();
    
    if (todo) {
      console.log(`๐Ÿ—‘๏ธ Deleted: ${categoryEmojis[todo.category]} ${todo.title}`);
    }
  };
  
  // ๐Ÿ’พ Local storage functions
  const saveTodos = (): void => {
    localStorage.setItem('typescript-todos', JSON.stringify(todos));
  };
  
  const loadTodos = (): void => {
    const saved = localStorage.getItem('typescript-todos');
    if (saved) {
      try {
        const parsed = JSON.parse(saved);
        todos = parsed.map((todo: any) => ({
          ...todo,
          createdAt: new Date(todo.createdAt),
          dueDate: todo.dueDate ? new Date(todo.dueDate) : undefined
        }));
      } catch (error) {
        console.error('Failed to load todos:', error);
      }
    }
  };
  
  // ๐ŸŽฎ Keyboard shortcuts
  const handleKeydown = (event: KeyboardEvent): void => {
    if (event.ctrlKey || event.metaKey) {
      switch (event.key) {
        case 'k':
          event.preventDefault();
          document.getElementById('search-input')?.focus();
          break;
        case 'n':
          event.preventDefault();
          document.getElementById('new-todo-input')?.focus();
          break;
      }
    }
  };
  
  // ๐Ÿš€ Initialize on mount
  onMount(() => {
    loadTodos();
  });
</script>

<svelte:window on:keydown={handleKeydown} />

<div class="todo-app">
  <header class="app-header">
    <h1>๐Ÿ“‹ TypeScript Todo App</h1>
    <div class="stats">
      <span>๐Ÿ“Š {completedCount}/{totalCount} completed ({completionRate}%)</span>
    </div>
  </header>
  
  <!-- ๐Ÿ” Search and filters -->
  <div class="controls">
    <input
      id="search-input"
      type="text"
      placeholder="๐Ÿ” Search todos... (Ctrl+K)"
      bind:value={searchTerm}
    />
    
    <select bind:value={filter}>
      <option value="all">All Todos</option>
      <option value="active">Active</option>
      <option value="completed">Completed</option>
    </select>
    
    <select bind:value={sortBy}>
      <option value="created">Sort by Created</option>
      <option value="priority">Sort by Priority</option>
      <option value="dueDate">Sort by Due Date</option>
      <option value="title">Sort by Title</option>
    </select>
  </div>
  
  <!-- โž• Add new todo -->
  <div class="add-todo">
    <input
      id="new-todo-input"
      type="text"
      placeholder="What needs to be done? (Ctrl+N)"
      bind:value={newTodoTitle}
      on:keydown={(e) => e.key === 'Enter' && addTodo()}
    />
    
    <select bind:value={selectedCategory}>
      <option value="personal">๐Ÿ  Personal</option>
      <option value="work">๐Ÿ’ผ Work</option>
      <option value="shopping">๐Ÿ›’ Shopping</option>
    </select>
    
    <select bind:value={selectedPriority}>
      <option value="low">๐ŸŸข Low</option>
      <option value="medium">๐ŸŸก Medium</option>
      <option value="high">๐Ÿ”ด High</option>
    </select>
    
    <button on:click={addTodo} disabled={!newTodoTitle.trim()}>
      Add Todo โž•
    </button>
  </div>
  
  <!-- ๐Ÿ“‹ Todos list -->
  <div class="todos-list">
    {#if filteredTodos.length === 0}
      <div class="empty-state">
        {#if searchTerm}
          <p>๐Ÿ” No todos found matching "{searchTerm}"</p>
        {:else if filter === 'completed'}
          <p>๐ŸŽ‰ No completed todos yet!</p>
        {:else if filter === 'active'}
          <p>โœจ All caught up! No active todos.</p>
        {:else}
          <p>๐Ÿ“ No todos yet. Add one above!</p>
        {/if}
      </div>
    {:else}
      {#each filteredTodos as todo (todo.id)}
        <div class="todo-item" class:completed={todo.completed}>
          <input
            type="checkbox"
            checked={todo.completed}
            on:change={() => toggleTodo(todo.id)}
          />
          
          <div class="todo-content">
            <div class="todo-header">
              <span class="category-emoji">{categoryEmojis[todo.category]}</span>
              <span class="todo-title" class:completed={todo.completed}>
                {todo.title}
              </span>
              <span 
                class="priority-badge"
                style="background-color: {priorityColors[todo.priority]}"
              >
                {todo.priority}
              </span>
            </div>
            
            <div class="todo-meta">
              <small>
                Category: {todo.category} โ€ข 
                Created: {todo.createdAt.toLocaleDateString()}
                {#if todo.dueDate}
                  โ€ข Due: {todo.dueDate.toLocaleDateString()}
                {/if}
              </small>
            </div>
          </div>
          
          <button class="delete-btn" on:click={() => deleteTodo(todo.id)}>
            ๐Ÿ—‘๏ธ
          </button>
        </div>
      {/each}
    {/if}
  </div>
</div>

<style>
  .todo-app {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  }
  
  .app-header {
    text-align: center;
    margin-bottom: 2rem;
  }
  
  .stats {
    color: #666;
    font-size: 0.9rem;
  }
  
  .controls {
    display: flex;
    gap: 1rem;
    margin-bottom: 1rem;
    flex-wrap: wrap;
  }
  
  .controls input, .controls select {
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    flex: 1;
    min-width: 150px;
  }
  
  .add-todo {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 2rem;
    flex-wrap: wrap;
  }
  
  .add-todo input {
    flex: 2;
    padding: 0.75rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
  }
  
  .add-todo select, .add-todo button {
    padding: 0.75rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    background: white;
    cursor: pointer;
  }
  
  .add-todo button {
    background: #007bff;
    color: white;
    border-color: #007bff;
    font-weight: 600;
  }
  
  .add-todo button:disabled {
    background: #6c757d;
    border-color: #6c757d;
    cursor: not-allowed;
  }
  
  .todos-list {
    margin-top: 2rem;
  }
  
  .empty-state {
    text-align: center;
    padding: 3rem;
    color: #666;
    font-style: italic;
  }
  
  .todo-item {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: 1rem;
    margin-bottom: 0.5rem;
    background: white;
    border: 1px solid #e9ecef;
    border-radius: 8px;
    transition: all 0.2s ease;
  }
  
  .todo-item:hover {
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  }
  
  .todo-item.completed {
    opacity: 0.7;
    background: #f8f9fa;
  }
  
  .todo-content {
    flex: 1;
  }
  
  .todo-header {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.25rem;
  }
  
  .category-emoji {
    font-size: 1.2rem;
  }
  
  .todo-title {
    flex: 1;
    font-weight: 500;
  }
  
  .todo-title.completed {
    text-decoration: line-through;
    color: #6c757d;
  }
  
  .priority-badge {
    color: white;
    padding: 0.25rem 0.5rem;
    border-radius: 12px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
  }
  
  .todo-meta {
    color: #6c757d;
    font-size: 0.8rem;
  }
  
  .delete-btn {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 1rem;
    opacity: 0.7;
    transition: opacity 0.2s ease;
  }
  
  .delete-btn:hover {
    opacity: 1;
  }
  
  input[type="checkbox"] {
    width: 1.2rem;
    height: 1.2rem;
    margin-top: 0.1rem;
  }
</style>

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered TypeScript with Svelte! Hereโ€™s what you can now do:

  • โœ… Set up TypeScript in Svelte projects with confidence ๐Ÿ’ช
  • โœ… Create type-safe components with proper prop typing ๐Ÿ›ก๏ธ
  • โœ… Use reactive statements with TypeScript inference ๐ŸŽฏ
  • โœ… Handle events safely with proper event typing ๐Ÿ›
  • โœ… Build complex applications with Svelte and TypeScript! ๐Ÿš€

Remember: TypeScript and Svelte together create an incredible developer experience! You get compile-time safety with runtime performance. ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered TypeScript with Svelte!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice building the todo app from the exercise
  2. ๐Ÿ—๏ธ Create a Svelte component library with TypeScript
  3. ๐Ÿ“š Explore SvelteKit for full-stack TypeScript development
  4. ๐ŸŒŸ Build a real project combining both technologies!

Remember: Every Svelte expert started with their first component. Keep building, keep learning, and most importantly, have fun creating amazing applications! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ