Skip to the solution
MediumFrontend10 min read

How do you make provide/inject type-safe?

1.1k views

The problem

Question presented to candidate: "In our TypeScript Vue app, inject('cart') comes back as unknown, so people cast it with as Cart and we have had runtime crashes when the provider was missing. How do you make provide and inject type-safe end to end?"

What a strong answer should cover:

  • Use a Symbol key typed as InjectionKey<T>, exported from one module, so provide(key, value) checks the value and inject(key) returns T | undefined instead of unknown.
  • inject is honestly typed T | undefined because a provider may be missing; handle it with a default (inject(key, fallback) returns T) or a useX() helper that throws a clear error.
  • String keys with a generic, inject<Cart>('cart'), and non-null assertions are only promises to the compiler, not checks.
  • A reusable createContext<T>() factory that returns a typed provide/use pair and hides the key.
  • Provide readonly state plus typed action functions, so the type also documents who may mutate.
  • Verify with vue-tsc, since templates and <script setup> are only checked by it, not by plain tsc.

Clarifying questions expected:

  • "Is there always a provider above every consumer, or are some consumers optional?"
  • "Is this application code or a library other teams consume?"

Code / implementation expected: Yes β€” an InjectionKey<T>, a typed provide, and a useX() helper (or a generic factory) that throws when the provider is missing.

  • provide
  • typed

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 10 sections

Read the code

VueA createContext() factory: private Symbol key, provide helper and a use helper that throws
<script setup>
import { ref, provide, inject, defineComponent, h } from "vue";

// One factory per context: a private Symbol key plus a provide helper and a use helper.
// In TypeScript the key is typed as InjectionKey<T>, so both helpers share one type.
function createContext(name) {
  const key = Symbol(name);
  const provideContext = (value) => (provide(key, value), value);
  const useContext = () => {
    const value = inject(key, null);
    if (value === null) throw new Error(`use${name}() needs a ${name} provider above it`);
    return value;
  };
  return [provideContext, useContext];
}

const [provideTheme, useTheme] = createContext("Theme");

const ThemeLabel = defineComponent({
  setup() {
    const theme = useTheme();
    return () => h("button", { class: "label", onClick: theme.toggle }, `theme: ${theme.mode.value}`);
  },
});

// Rendered outside any provider: the helper fails loudly instead of returning undefined.
const Orphan = defineComponent({
  setup() {
    let message;
    try { useTheme(); } catch (e) { message = e.message; }
    return () => h("p", { class: "orphan" }, `orphan: ${message}`);
  },
});

const ThemeProvider = defineComponent({
  setup(_, { slots }) {
    const mode = ref("light");
    provideTheme({ mode, toggle: () => (mode.value = mode.value === "light" ? "dark" : "light") });
    return () => h("section", slots.default());
  },
});

// A string key that happens to match the Symbol description finds nothing.
const sameName = inject("Theme", "no provider for the string key");
</script>

<template>
  <ThemeProvider><ThemeLabel /></ThemeProvider>
  <Orphan />
  <p class="string">{{ sameName }}</p>
</template>
keys.ts: InjectionKey<T> keys with typed provide and use helpers
import { inject, provide, type InjectionKey, type Ref } from "vue";

export interface Cart {
  items: Ref<string[]>;
  add: (sku: string) => void;
}
export const CartKey: InjectionKey<Cart> = Symbol("cart");
export const LocaleKey = Symbol("locale") as InjectionKey<string>;

export function provideCart(cart: Cart) {
  provide(CartKey, cart);
}
export function useCart(): Cart {
  const cart = inject(CartKey);
  if (!cart) throw new Error("useCart() needs a <CartProvider> ancestor");
  return cart;
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 27 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

Why use storeToRefs with Pinia?

Continue