A Vue-style defineComponent where this means the right thing in data, computed and methods: three inferred shapes fed back into the same object literal via ThisType.
Vue infers a different this for each block of your component options; here you build the types that make that work.
The task is a function similar to Vue.extend or defineComponent that properly infers the this type inside computed and methods. The options object has three fields: data is a function returning an object whose properties become part of the context (but this must not be usable inside data itself), computed is an object of functions that can read the data via this and whose return values are exposed on the context, and methods are functions that can access data, computed values, and other methods through this. The return type of SimpleVue itself can be anything.
Solving it teaches you ThisType<T>, the marker type behind Vue's and Pinia's ergonomic APIs, and shows how TypeScript's contextual typing lets one part of an object literal shape the this seen in another part.
const instance = SimpleVue({
data() {
return {
firstname: 'Type',
lastname: 'Challenges',
amount: 10,
}
},
computed: {
fullname() {
return this.firstname + ' ' + this.lastname
}
},
methods: {
hi() {
alert(this.fullname.toLowerCase())
}
}
})Implement a simpiled version of a Vue-like typing support.
By providing a function name SimpleVue (similar to Vue.extend or defineComponent), it should properly infer the this type inside computed and methods.
In this challenge, we assume that SimpleVue take an Object with data, computed and methods fields as it's only argument,
data is a simple function that returns an object that exposes the context this, but you won't be accessible to other computed values or methods.
computed is an Object of functions that take the context as this, doing some calculation and returns the result. The computed results should be exposed to the context as the plain return values instead of functions.
methods is an Object of functions that take the context as this as well. Methods can access the fields exposed by data, computed as well as other methods. The different between computed is that methods exposed as functions as-is.
The type of SimpleVue's return value can be arbitrary.
const instance = SimpleVue({
data() {
return {
firstname: 'Type',
lastname: 'Challenges',
amount: 10,
}
},
computed: {
fullname() {
return this.firstname + ' ' + this.lastname
}
},
methods: {
hi() {
alert(this.fullname.toLowerCase())
}
}
})View on GitHub: https://tsch.js.org/6
Change the following code to make the test cases pass (no type check errors).
The declaration in full:
type GetComputed<TComputed> = {
[K in keyof TComputed]: TComputed[K] extends () => infer Result
? Result
: never
}
declare function SimpleVue<TData, TComputed, TMethods>(options: {
data: (this: void) => TData
computed: TComputed & ThisType<TData>
methods: TMethods & ThisType<TData & GetComputed<TComputed> & TMethods>
}): anyThree generic parameters, one helper type, and one unusual built-in: ThisType.
SimpleVue<TData, TComputed, TMethods> doesn't require you to pass any type arguments. TypeScript infers all three from the object literal you call it with:
TData is inferred from the return type of the data function.TComputed is inferred as the object type of everything you wrote in computed.TMethods is inferred as the object type of everything you wrote in methods.For the example above, that means:
// TData = { firstname: string; lastname: string; amount: number }
// TComputed = { fullname: () => string }
// TMethods = { hi: () => void }These inferred types are immediately fed back into the same options object to type this. Inference and contextual typing happen in a single pass.
this inside dataThe test suite demands that this.firstname, this.getRandom() and even this.data() are all errors inside data. The fix is the signature data: (this: void) => TData. Declaring a this parameter of type void tells TypeScript that the function must not use this at all, so any property access on it becomes a compile error. (A this parameter is a pseudo-parameter: it only exists in the type system and never affects the emitted JavaScript.)
ThisType<T>: the contextual this markerThisType<T> is an empty marker interface from lib.d.ts with one piece of special compiler support: when an object literal is contextually typed by X & ThisType<T>, every function inside that literal gets this: T, without you writing a this parameter on each function. Note that this only takes effect when the noImplicitThis compiler option is enabled (it is, under strict); without it, ThisType is inert and this stays any.
So computed: TComputed & ThisType<TData> means: infer the shape of the computed object as TComputed, and while doing so, type this inside those functions as the data object. That's why this.firstname works inside fullname().
GetComputedInside methods, this.fullname must be a string, not a () => string: computed properties are exposed as plain values. GetComputed performs that unwrap with a mapped type plus infer:
type Example = GetComputed<{ fullname: () => string }>
// evaluates to: { fullname: string }For each key, TComputed[K] extends () => infer Result ? Result : never asks "is this a zero-argument function?" and, if so, extracts its return type into Result. The infer keyword introduces a new type variable right inside the conditional check. TypeScript figures out what Result must be for the pattern to match.
this for methodsMethods see everything, so their contextual this is the intersection of all three worlds:
[object Object]That's data properties, unwrapped computed values, and the other methods, merged into one object type. This is why the test method hi() can call alert(this.amount) (data), this.fullname.toLowerCase() (a computed exposed as string), and this.getRandom() (another method), and why the Equal<typeof fullname, string> assertion passes.
// @ts-expect-error this.firstname inside data: handled by the this: void parameter; the error must occur or the @ts-expect-error itself fails.this.fullname.toLowerCase() in a method only compiles because GetComputed turned () => string into string.this.getRandom() works because TMethods itself is part of the methods' ThisType intersection.Note that in computed the challenge only requires access to data (ThisType<TData>); real Vue also lets computed properties read other computed properties, which you could model with ThisType<TData & GetComputed<TComputed>>. The tests here don't require it, so the simpler form keeps the solution honest and readable.
This challenge is originally from here.