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.