#1290Hard

Pinia

Type Pinia's defineStore: getters are written as functions but read as values, and state is mutable in actions but read-only in getters. ThisType does the work.

Getters become values, state flips between mutable and read-only depending on where you stand, and ThisType decides who sees what.

The target is a type-level version of Pinia's defineStore. No implementation needed, just types. The function takes a single object with four properties: id (a string), state (a function returning the store's state), getters (computed-style methods) and actions (methods that can mutate state). Getters are defined as functions but consumed as plain values, and inside a getter the state is read-only:

const store = defineStore({
  // ...other required fields
  getters: {
    getSomething() {
      return 'xxx'
    }
  }
})
 
store.getSomething    // ok
store.getSomething()  // error

Actions stay callable functions. Their parameter and return types must survive intact so call sites are fully type-checked. Inside an action, this can read and mutate state and call other actions. Getters are visible there too, still read-only:

const store = defineStore({
  // ...other required fields
  actions: {
    doSideEffect() {
      this.xxx = 'xxx'
      return 'ok'
    }
  }
})
 
const returnValue = store.doSideEffect()

Challenge Instructions: Pinia

Hard

Create a type-level function whose types is similar to Pinia library. You don't need to implement function actually, just adding types.

Overview

This function receive only one parameter whose type is an object. The object contains 4 properties:

  • id - just a string (required)
  • state - a function which will return an object as store's state (required)
  • getters - an object with methods which is similar to Vue's computed values or Vuex's getters, and details are below (optional)
  • actions - an object with methods which can do side effects and mutate state, and details are below (optional)

Getters

When you define a store like this:

const store = defineStore({
// ...other required fields
getters: {
getSomething() {
return 'xxx'
}
}
})

And you should use it like this:

[object Object]

instead of:

[object Object]

Additionally, getters can access state and/or other getters via this, but state is read-only.

Actions

When you define a store like this:

const store = defineStore({
// ...other required fields
actions: {
doSideEffect() {
this.xxx = 'xxx'
return 'ok'
}
}
})

Using it is just to call it:

[object Object]

Actions can return any value or return nothing, and it can receive any number of parameters with different types. Parameters types and return type can't be lost, which means type-checking must be available at call side.

State can be accessed and mutated via this. Getters can be accessed via this but they're read-only.

View on GitHub: https://tsch.js.org/1290

Change the following code to make the test cases pass (no type check errors).

👋 Lifetime-License is leaving on August 10, 2026

Get it now for $29

One-time payment. Lifetime access to all pro challenges.

Loading...

Detailed Explanation

The full solution:

type GetterValues<TGetters> = {
  readonly [K in keyof TGetters]: TGetters[K] extends () => infer Result
    ? Result
    : never
}
 
declare function defineStore<TState, TGetters, TActions>(store: {
  id: string
  state: () => TState
  getters: TGetters & ThisType<Readonly<TState> & GetterValues<TGetters>>
  actions: TActions &
    ThisType<TState & GetterValues<TGetters> & TActions>
}): TState & GetterValues<TGetters> & TActions

The challenge comes down to one question asked three times: what is visible, and what is mutable, through this or the store in a given context.

Inferring the three shapes

defineStore<TState, TGetters, TActions> gets all three type arguments inferred from the object literal you pass in: TState from the return type of state, and TGetters/TActions from the literal shapes of those objects. For the test store:

// TState   = { num: number; str: string }
// TGetters = { stringifiedNum: () => string; parsedNum: () => number }
// TActions = { init: () => void; increment: (step?: number) => void; ... }

GetterValues: functions in, values out

Getters are written as functions but read as values. store.stringifiedNum is a string, and store.stringifiedNum() must be an error. GetterValues does the conversion with a mapped type:

type Example = GetterValues<{ stringifiedNum: () => string }>
// evaluates to: { readonly stringifiedNum: string }

For each key, TGetters[K] extends () => infer Result extracts the function's return type. Two details matter here:

ThisType: a different this per context

ThisType<T> is an empty marker interface with special compiler support: when an object literal is contextually typed by X & ThisType<T>, every method in that literal gets this: T automatically. (This requires the noImplicitThis compiler option, which is on under strict; without it, ThisType has no effect.) The solution uses it twice, with deliberately different Ts.

Getters get ThisType<Readonly<TState> & GetterValues<TGetters>>:

Actions get ThisType<TState & GetterValues<TGetters> & TActions>:

The return type: the public store

The store you hand back is the same intersection the actions see:

[object Object]

State properties come through as-is (store.num is number), getters as unwrapped read-only values, and actions as untouched functions. Keeping TActions untouched is what preserves signatures exactly: increment(step = 1) is inferred as (step?: number) => void, so store.increment() and store.increment(2) both work while store.init(0), a call with an argument to a zero-parameter action, fails.

Edge cases the tests cover

If you solved Simple Vue, this is the same trick with sharper edges. The mechanics of ThisType are not the hard part; deciding which capabilities each context gets to see is.

This challenge is originally from here.

Share this challenge

Learn the Concepts