Skip to content

fix: stricter type narrowing for head schema#665

Merged
harlan-zw merged 2 commits into
v3from
fix/stricter-type-narrowing
Feb 24, 2026
Merged

fix: stricter type narrowing for head schema#665
harlan-zw merged 2 commits into
v3from
fix/stricter-type-narrowing

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

@harlan-zw harlan-zw commented Feb 24, 2026

🔗 Linked issue

Related to #527

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

The head schema types were too loose — <base> accepted empty objects, <link> didn't narrow by rel, and attributes like lang/inputmode/target were plain strings with no autocomplete. This tightens type narrowing across the schema so that discriminated unions (e.g. Link by rel, Base requiring href or target, Meta by name/property/http-equiv) provide better IDE autocomplete and catch invalid combinations at the type level.

Examples of code that now errors

Empty <base> (must provide href or target)

// ❌ TS error — Base requires at least `href` or `target`
useHead({ base: {} })

// ✅
useHead({ base: { href: '/' } })
useHead({ base: { target: '_blank' } })

Mixing name + property on <meta>

// ❌ TS error — `property` is `never` on NameMeta
useHead({
  meta: [{ name: 'description', property: 'og:title', content: 'Hello' }]
})

// ✅ separate entries
useHead({
  meta: [
    { name: 'description', content: 'Hello' },
    { property: 'og:title', content: 'Hello' },
  ]
})

charset meta with content

// ❌ TS error — `content` is `never` on CharsetMeta
useHead({
  meta: [{ charset: 'utf-8', content: 'text/html' }]
})

// ✅
useHead({
  meta: [{ charset: 'utf-8' }]
})

External script with textContent

// ❌ TS error — `textContent` is `never` when `src` is present
useHead({
  script: [{ src: '/app.js', textContent: 'console.log("hi")' }]
})

// ✅ pick one
useHead({ script: [{ src: '/app.js' }] })
useHead({ script: [{ textContent: 'console.log("hi")' }] })

JSON-LD with src

// ❌ TS error — `src` is `never` on JsonLdScript
useHead({
  script: [{ type: 'application/ld+json', src: '/schema.json' }]
})

// ✅
useHead({
  script: [{ type: 'application/ld+json', textContent: { '@type': 'WebSite' } }]
})

<link rel="mask-icon"> missing required color

// ❌ TS error — `color` is required on MaskIconLink
useHead({
  link: [{ rel: 'mask-icon', href: '/icon.svg' }]
})

// ✅
useHead({
  link: [{ rel: 'mask-icon', href: '/icon.svg', color: '#000' }]
})

<noscript> with both textContent and innerHTML

// ❌ TS error — can't provide both (one must be `never`)
useHead({
  noscript: [{ textContent: 'JS disabled', innerHTML: '<p>JS disabled</p>' }]
})

// ✅ pick one
useHead({ noscript: [{ textContent: 'JS disabled' }] })
useHead({ noscript: [{ innerHTML: '<p>JS disabled</p>' }] })

<link> without href (GenericLink now requires it)

// ❌ TS error — `href` is required on GenericLink
useHead({
  link: [{ rel: 'stylesheet' }]
})

// ✅
useHead({
  link: [{ rel: 'stylesheet', href: '/style.css' }]
})

@harlan-zw harlan-zw merged commit 0c8573f into v3 Feb 24, 2026
1 check failed
@harlan-zw harlan-zw mentioned this pull request Apr 4, 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.

1 participant