TypeScript Issue: shopify.app.extensions() fails to narrow types

Hi everyone,

I’m seeing a TypeScript regression in the @shopify/app-bridge-types package. The ExtensionInfo interface is not a proper discriminated union, making it impossible to narrow the activations property.

The Problem

Even after checking type === 'theme_app_extension', TypeScript cannot access properties like handle inside the activations array. It incorrectly defaults to the UiExtensionActivation type.

Example:

const extensions = await globalThis.shopify.app.extensions();
const themeExtension = extensions.find(ext => ext.type === 'theme_app_extension');

if (themeExtension) {
  themeExtension.activations.forEach((block) => {
    // Error: Property 'handle' does not exist on type 'UiExtensionActivation'.
    console.log(block.handle); 
  });
}

Why this happens

The ExtensionInfo interface uses a generic with a conditional type for activations:

activations: Type extends 'ui_extension' ? UiExtensionActivation[] : Type extends 'theme_app_extension' ? ThemeExtensionActivation[] : never;

Because this is an interface and not a union of distinct types, the activations property remains a broken union that cannot be narrowed by checking the type field.


Is this a known bug? It would be great if ExtensionInfo was refactored into a standard discriminated union so it works out of the box.

I am using this Vue 3 implementation as a workaround. It requires manual casting because the library does not narrow the extension type based on the type property.

type ExtensionInfo = Awaited<ReturnType<typeof globalThis.shopify.app.extensions>>

const extensions = shallowRef<ExtensionInfo>([])
const extensionsLoading = ref(true)

// Manual assertion is required here because type narrowing fails
const themeExtensionActivations = computed(() => (
  extensions.value.find(extension => extension.type === 'theme_app_extension')?.activations as {
    handle: string
    status: 'active' | 'available' | 'unavailable'
  }[] ?? []
))

const appEmbedEnabled = computed(() => (
  themeExtensionActivations.value.find(activation => (
    activation.handle === 'core'
  ))?.status === 'active'
))

const appBlockEnabled = computed(() => (
  themeExtensionActivations.value.find(activation => (
    activation.handle === 'widget'
  ))?.status === 'active'
))

async function loadExtensions() {
  extensionsLoading.value = true
  extensions.value = await globalThis.shopify.app.extensions()
  extensionsLoading.value = false
}

onMounted(loadExtensions)

Key Issues

  • Type Safety: Using as bypasses the compiler. Changes to the API schema will cause runtime failures instead of build errors.

  • Discriminated Unions: The extension.type check fails to narrow the object shape, losing autocompletion and safety.

  • Maintenance: Manually defining the activations interface is redundant and risks drifting from the official API spec.

  • Type Accessibility: The need for complex utility types suggests the internal API types should be exported directly.