Extend Simple Vue with a props option: infer prop types from constructors like Boolean, String or custom classes, including unions from constructor arrays.
This challenge continues from 6 - Simple Vue, you should finish that one first, and modify your code based on it to start this challenge. On top of the data/computed/methods trio, the options object now gets a props field: an object whose keys become real props injected into this, accessible in data, computed and methods. Each prop is declared either directly as a constructor or as an object with a type field containing one or more constructors, and your job is to translate those runtime values into compile-time types.
props: {
foo: Boolean
}
// or
props: {
foo: { type: Boolean }
}should be inferred to type Props = { foo: boolean }.
When passing multiple constructors, the type should be inferred to a union:
props: {
foo: { type: [Boolean, Number, String] }
}
// -->
type Props = { foo: boolean | number | string }When an empty object is passed, the key should be inferred to any. (required, default, and array props in Vue are not considered in this challenge.)
This challenge continues from 6 - Simple Vue, you should finish that one first, and modify your code based on it to start this challenge*.
In addition to the Simple Vue, we are now having a new props field in the options. This is a simplified version of Vue's props option. Here are some of the rules.
props is an object containing each field as the key of the real props injected into this. The injected props will be accessible in all the context including data, computed, and methods.
A prop will be defined either by a constructor or an object with a type field containing constructor(s).
For example
props: {
foo: Boolean
}
// or
props: {
foo: { type: Boolean }
}should be inferred to type Props = { foo: boolean }.
When passing multiple constructors, the type should be inferred to a union.
props: {
foo: { type: [Boolean, Number, String] }
}
// -->
type Props = { foo: boolean | number | string }When an empty object is passed, the key should be inferred to any.
For more specified cases, check out the Test Cases section.
required,default, and array props in Vue are not considered in this challenge.
View on GitHub: https://tsch.js.org/213
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.
The complete solution:
type ExtractInstance<T> = T extends (...args: any[]) => infer R
? R
: T extends new (...args: any[]) => infer R
? R
: any
type InferProp<T> = T extends { type: infer U }
? U extends unknown[]
? ExtractInstance<U[number]>
: ExtractInstance<U>
: ExtractInstance<T>
type InferProps<TProps> = {
[K in keyof TProps]: InferProp<TProps[K]>
}
type GetComputed<TComputed> = {
[K in keyof TComputed]: TComputed[K] extends () => infer Result
? Result
: never
}
declare function VueBasicProps<TProps, TData, TComputed, TMethods>(options: {
props: TProps
data: (this: InferProps<TProps>) => TData
computed: TComputed & ThisType<TData & InferProps<TProps>>
methods: TMethods &
ThisType<TData & InferProps<TProps> & GetComputed<TComputed> & TMethods>
}): anyThe SimpleVue skeleton is unchanged. All the new work happens in turning a props declaration into a props type, so start with the smallest piece.
ExtractInstance: from constructor to instance typeA prop is declared with a runtime value like String or ClassA. The type of the value String is StringConstructor, and the type of the value ClassA is typeof ClassA. Both are things you can invoke, and we want what invoking them produces.
The order of the two checks is the subtle part:
StringConstructor, BooleanConstructor and friends have plain call signatures like (value?: any) => string. Matching those first is what gets you the primitive string, boolean, number. If you matched new (...) => infer R first, you'd infer the wrapper object types String, Boolean, Number, and the Equal<PropsType['propB'], string> test would fail.ClassA has no call signature (you can't invoke a class without new), so it falls through to new (...args: any[]) => infer R, giving the instance type ClassA.any as the fallback. Anything that's neither callable nor constructible, like the empty object in propA: {}, becomes any, exactly what the IsAny test demands.A quick sanity check:
type A = ExtractInstance<StringConstructor> // string (call signature wins)
type B = ExtractInstance<typeof ClassA> // ClassA (construct signature)
type C = ExtractInstance<{}> // any (fallback)InferProp: the three declaration shapesInferProp dispatches on how the prop was written:
{ type: [String, Number] }: matches { type: infer U }. Because TProps is a bare, unconstrained type parameter, TypeScript infers the array literal as the array type (StringConstructor | NumberConstructor)[], not a tuple, so the element type is already a union of constructors. U extends unknown[] matches, and U[number] reads out that element union (this indexing works the same whether U is an array or a tuple, so the solution wouldn't change if a tuple were inferred). Then, because T is a naked type parameter in ExtractInstance, the conditional type distributes over that union: each constructor is converted separately and the results are unioned back together as string | number. Distribution over unions is doing the heavy lifting here. You get the union conversion for free.{ type: Boolean }: matches { type: infer U } with a single constructor; ExtractInstance<U> handles it directly.RegExp (bare constructor): doesn't have a type property, so it falls to the outer else branch and is converted as-is. Note RegExpConstructor does have a call signature (calling RegExp('a') works) and it returns RegExp, so the call-first ordering still gives the right answer.{}: has no type property either, so it also hits ExtractInstance<{}>, which bottoms out at any.InferProps then maps InferProp over every key, turning the whole declaration object into { propA: any; propB: string; ... }.
Compared to Simple Vue, each this gains InferProps<TProps>:
data: (this: InferProps<TProps>) => TData. In Simple Vue, data had this: void. Now this is exactly the props object: the test can read this.propE inside data, while this.firstname (data isn't defined yet at that point) and this.data() remain @ts-expect-errors because they don't exist on InferProps<TProps>.computed gets ThisType<TData & InferProps<TProps>>: data plus props.methods gets the full intersection: data, props, unwrapped computed values (GetComputed, unchanged from Simple Vue), and the other methods.propA: {} must be any, not unknown or never. Verified with IsAny, which only any satisfies.propD: { type: ClassA } proves your solution isn't hardcoded to primitive constructors; any class works via the construct-signature branch.propE: { type: [String, Number] } inside methods (Equal<typeof propE, string | number>) confirms props flow through every context, not just data.@ts-expect-error lines in data guard against making this too permissive: if this were any, those expected errors would vanish and the test would fail.The takeaway: one careful conditional type (ExtractInstance) plus distribution over unions is all it takes to bridge Vue's runtime prop declarations into precise static types.
This challenge is originally from here.