Overview
Code review of app-launcher.ts from React Core and TypeScript (Anders Hejlsberg) perspectives reveals critical type safety issues and imperative patterns that hurt maintainability.
🔴 Critical Issues
1. Type Safety Violations (Line 73)
Problem: as any cast completely bypasses TypeScript's type system
appsDb.choices as any // Line 73
Impact:
- Destroys IDE autocomplete and refactoring capabilities
- Runtime errors become inevitable when shape of
choices changes
- No IntelliSense or safety net
Solution: Type appsDb properly:
interface DbResult<T> {
choices: T[]
}
// Option 1: Type the result
const appsDb: DbResult<Choice> = await db(...)
// Option 2: At minimum
appsDb.choices as Choice[] // with comment explaining why
2. Implicit Any Types (Lines 48, 61)
Problem: Multiple implicit any types throughout:
input => { ... } (line 61) - what's the type of input?
- Flag values have no type definition
Solution: Define a Flags interface:
interface AppLauncherFlags {
prep?: boolean
input?: string
refresh?: boolean
}
🟡 Major Concerns
3. Imperative State Mutations (Lines 24-35)
Problem: Direct mutation of UI state through multiple imperative calls:
setResize(true)
setChoices([{ name: "...", info: true }])
clearTabs()
setPlaceholder("One sec...")
React perspective: This is the antithesis of declarative UI. State changes are scattered and unpredictable with no single source of truth.
Solution: Use declarative state pattern:
const loadingState = {
resize: true,
placeholder: "One sec...",
choices: [{ name: "First Run: ...", info: true }],
tabs: []
}
4. Side Effect Chaos (Lines 17-18)
Problem: Side effect triggered by conditional without clear data flow:
if (!flag.prep) {
preload() // Hidden dependencies and mutations
}
React principle: Side effects should be explicit, predictable, and well-contained.
5. Unclear Data Flow (Lines 21-43)
Problem: The db() function pattern obscures actual data flow:
let appsDb = await db(
kitPath("db", "apps.json"),
async () => { /* side effects + data loading */ },
!flag?.refresh // Cache control via boolean
)
Issues:
- Is this a cache? A lazy loader? Both?
- Second argument is a factory function with side effects
- Third argument inverts cache logic (
!flag?.refresh is confusing)
- No way to predict when UI updates happen
6. Missing Type Guards (Line 75)
Problem: Check for app without type narrowing:
What type is app? Could be string | Choice | undefined. Without proper typing, open(app) might fail at runtime.
🟢 Moderate Issues
7. Magic Strings and Stringly-Typed Code (Lines 48-73)
Problem: String literals everywhere without type safety:
key: "app-launcher" // Line 50
kitPath("main", "app-launcher.js") // Line 64
"--input", "--refresh" // Lines 65, 67
Solution:
enum AppLauncherKey {
Key = "app-launcher",
ScriptPath = "main/app-launcher.js"
}
enum Flag {
Input = "--input",
Refresh = "--refresh",
Prep = "--prep"
}
8. Poor Discoverability (Lines 54-71)
Problem: Shortcuts object is nested and hard to discover.
Solution: Extract to well-typed constant:
interface Shortcut {
name: string
visible: boolean
key: string
bar: 'left' | 'right'
onPress: (input: string) => Promise<void>
}
const REFRESH_SHORTCUT: Shortcut = { ... }
9. Platform Check Without Abstraction (Lines 8-15)
Problem: Early exit for Linux without abstraction:
if (isLinux) {
await div(md("..."))
exit()
}
Better: Extract to typed PlatformCheck function that returns Result<void, UnsupportedPlatformError>.
🔵 Minor Observations
10. Inconsistent Async Handling
- Line 18:
preload() called without await (fire-and-forget?)
- Lines 21, 48, 63, 76: Other async calls use
await
Is preload() intentionally fire-and-forget? If so, add a comment.
11. Function Call Without Import Context
Lines call global functions (div, md, exit, preload, setResize, arg, open) without imports. Add reference comment:
// Global runtime functions injected by Script Kit
// See: sdk/src/globals/...
✅ What's Good
- Clear intent - Script's purpose is obvious from comments and structure
- User feedback - Good loading messages for first-run experience (lines 27-29)
- Caching strategy - The
db() pattern provides reasonable caching (line 42)
- Platform awareness - Correctly checks
isLinux early (line 8)
📋 Recommendations
Immediate (P0):
- Remove
as any - Type the appsDb.choices properly
- Define
Flags interface - Make flag types explicit
- Type the
onPress callback - What's the type of input?
Short-term (P1):
- Extract UI state - Create a
LoadingState type describing the full UI
- Document globals - Add a comment explaining the injected runtime
- Add type guard for
app - Narrow the return type of arg()
Long-term (P2):
- Refactor to declarative pattern - Consider modeling this as state + reducer
- Extract constants - Replace magic strings with typed enums
- Create Shortcut type - Make shortcuts discoverable and type-safe
🎯 Severity Summary
High Impact:
- Type safety (
as any cast) - Breaks tooling, invites runtime errors
- Imperative mutations - Makes code hard to reason about
Medium Impact:
- Missing type definitions - Hurts developer experience
- Unclear data flow - Maintenance burden
Low Impact:
- Magic strings - Refactoring friction
- Documentation gaps - Onboarding friction
❓ Questions
- What is the actual return type of
db()? Can we type it properly?
- Is
preload() intentionally fire-and-forget?
- Where are
setResize, setChoices, etc. defined? Can we import them?
- What's the type contract for the
arg() function's return value?
- Why
!flag?.refresh instead of flag?.refresh === true for clarity?
Final Verdict
- TypeScript grade: C- - Types exist but are actively bypassed with
as any
- React principles grade: D+ - Imperative mutations dominate; no clear data flow
This code works, but it's fighting TypeScript instead of leveraging it, and it's imperative where it could be declarative. With proper typing and state modeling, this could be excellent.
Overview
Code review of
app-launcher.tsfrom React Core and TypeScript (Anders Hejlsberg) perspectives reveals critical type safety issues and imperative patterns that hurt maintainability.🔴 Critical Issues
1. Type Safety Violations (Line 73)
Problem:
as anycast completely bypasses TypeScript's type systemImpact:
choiceschangesSolution: Type
appsDbproperly:2. Implicit Any Types (Lines 48, 61)
Problem: Multiple implicit
anytypes throughout:input => { ... }(line 61) - what's the type ofinput?Solution: Define a
Flagsinterface:🟡 Major Concerns
3. Imperative State Mutations (Lines 24-35)
Problem: Direct mutation of UI state through multiple imperative calls:
React perspective: This is the antithesis of declarative UI. State changes are scattered and unpredictable with no single source of truth.
Solution: Use declarative state pattern:
4. Side Effect Chaos (Lines 17-18)
Problem: Side effect triggered by conditional without clear data flow:
React principle: Side effects should be explicit, predictable, and well-contained.
5. Unclear Data Flow (Lines 21-43)
Problem: The
db()function pattern obscures actual data flow:Issues:
!flag?.refreshis confusing)6. Missing Type Guards (Line 75)
Problem: Check for
appwithout type narrowing:What type is
app? Could bestring | Choice | undefined. Without proper typing,open(app)might fail at runtime.🟢 Moderate Issues
7. Magic Strings and Stringly-Typed Code (Lines 48-73)
Problem: String literals everywhere without type safety:
Solution:
8. Poor Discoverability (Lines 54-71)
Problem: Shortcuts object is nested and hard to discover.
Solution: Extract to well-typed constant:
9. Platform Check Without Abstraction (Lines 8-15)
Problem: Early exit for Linux without abstraction:
Better: Extract to typed
PlatformCheckfunction that returnsResult<void, UnsupportedPlatformError>.🔵 Minor Observations
10. Inconsistent Async Handling
preload()called withoutawait(fire-and-forget?)awaitIs
preload()intentionally fire-and-forget? If so, add a comment.11. Function Call Without Import Context
Lines call global functions (
div,md,exit,preload,setResize,arg,open) without imports. Add reference comment:✅ What's Good
db()pattern provides reasonable caching (line 42)isLinuxearly (line 8)📋 Recommendations
Immediate (P0):
as any- Type theappsDb.choicesproperlyFlagsinterface - Make flag types explicitonPresscallback - What's the type ofinput?Short-term (P1):
LoadingStatetype describing the full UIapp- Narrow the return type ofarg()Long-term (P2):
🎯 Severity Summary
High Impact:
as anycast) - Breaks tooling, invites runtime errorsMedium Impact:
Low Impact:
❓ Questions
db()? Can we type it properly?preload()intentionally fire-and-forget?setResize,setChoices, etc. defined? Can we import them?arg()function's return value?!flag?.refreshinstead offlag?.refresh === truefor clarity?Final Verdict
as anyThis code works, but it's fighting TypeScript instead of leveraging it, and it's imperative where it could be declarative. With proper typing and state modeling, this could be excellent.