Skip to content

Conversation

@harlan-zw
Copy link
Collaborator

@harlan-zw harlan-zw commented Jan 3, 2026

πŸ”— Linked issue

❓ Type of change

  • πŸ“– Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • πŸ‘Œ Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

πŸ“š Description

Implements discriminated union types for useHead() input, narrowing types based on discriminator properties.

⚠️ May cause end user type errors.

Before / After

Link: rel narrows available properties

Before: all properties available on all links

useHead({
  link: [{
    rel: 'canonical',
    href: '/page',
    imagesrcset: '...',  // no error - but invalid for canonical!
    onload: () => {},    // no error - but canonical doesn't fire events!
  }]
})

After: CanonicalLink only has relevant props

useHead({
  link: [{
    rel: 'canonical',
    href: '/page',
    imagesrcset: '...',  // ❌ Error: Property 'imagesrcset' does not exist
    onload: () => {},    // ❌ Error: Property 'onload' does not exist
  }]
})

Link: preload as narrows further

Before: imagesrcset available on all preloads

useHead({
  link: [{ rel: 'preload', as: 'script', href: '/app.js', imagesrcset: '...' }]  // no error
})

After: imagesrcset only on PreloadImageLink

useHead({
  link: [{ rel: 'preload', as: 'image', href: '/hero.webp', imagesrcset: '...' }]  // βœ… valid
})
useHead({
  link: [{ rel: 'preload', as: 'script', href: '/app.js', imagesrcset: '...' }]   // ❌ Error
})

Link: font preload requires crossorigin

Before: no enforcement

useHead({
  link: [{ rel: 'preload', as: 'font', href: '/font.woff2' }]  // no error - but broken!
})

After: PreloadFontLink requires crossorigin

useHead({
  link: [{ rel: 'preload', as: 'font', href: '/font.woff2' }]  // ❌ Error: crossorigin required
})
useHead({
  link: [{ rel: 'preload', as: 'font', href: '/font.woff2', crossorigin: 'anonymous' }]  // βœ…
})

Script: type narrows available properties

Before: async/defer available on JSON-LD

useHead({
  script: [{ type: 'application/ld+json', textContent: {...}, async: true }]  // no error - invalid!
})

After: JsonLdScript only has textContent

useHead({
  script: [{ type: 'application/ld+json', textContent: {...}, async: true }]  // ❌ Error
})

Script: events only on external scripts

Before: onload on inline scripts

useHead({
  script: [{ textContent: 'console.log("hi")', onload: () => {} }]  // no error - never fires!
})

After: InlineScript has no event handlers

useHead({
  script: [{ textContent: 'console.log("hi")', onload: () => {} }]  // ❌ Error
})
useHead({
  script: [{ src: '/app.js', onload: () => {} }]  // βœ… ExternalScript has events
})

Meta: mutual exclusion of name/property/http-equiv/charset

Before: can use name + property together

useHead({
  meta: [{ name: 'description', property: 'og:description', content: '...' }]  // no error - invalid HTML!
})

After: discriminated union prevents this

useHead({
  meta: [{ name: 'description', property: 'og:description', content: '...' }]  // ❌ Error
})
useHead({
  meta: [{ name: 'description', content: '...' }]       // βœ… NameMeta
})
useHead({
  meta: [{ property: 'og:description', content: '...' }] // βœ… PropertyMeta
})

Summary

Type Discriminator Narrowed Types
Link rel StylesheetLink, PreloadLink, CanonicalLink, IconLink, etc.
Link (preload) as PreloadImageLink, PreloadFontLink, PreloadScriptLink, etc.
Script type/src ExternalScript, ModuleScript, JsonLdScript, InlineScript, etc.
Meta name/property/http-equiv/charset NameMeta, PropertyMeta, HttpEquivMeta, CharsetMeta

Legacy aliases (LinkWithoutEvents, ScriptWithoutEvents) preserved via GenericLink/GenericScript.

@harlan-zw
Copy link
Collaborator Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

πŸ€– Generated with Claude Code

@harlan-zw harlan-zw merged commit e8086a8 into v3 Jan 3, 2026
1 check passed
@harlan-zw harlan-zw mentioned this pull request Jan 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants