3

I have a defineComponent() function that takes an object as an argument. That object can contain a props property, and a setup() property that receives a props argument, whose type is inferred from the props property declared earlier in the object.

Example:

defineComponent({
  props: {
    size: {
      type: String,
      default: 'lg',
      validator: (value: string) => true,
    }
  },
  setup(props) {
    console.log(props.size.charAt(0))
  }
})

Oddly, if props.size.validator is declared as a regular function (validator: function(value: string) { return true }) instead of an arrow function, the inferred type for the props argument is completely wrong. For the above example. it should be:

{
  size: string
}

But it's this:

string[] | {
    [x: string]: Prop<unknown, unknown> | null;
}

The regular function is essentially the same as the arrow function in the working code, so I wasn't expecting it to break the type inference.

Why does a regular function break the type inference for setup()'s props argument?


mylib.d.ts:

declare interface PropOptions<T = any, D = T> {
    type?: PropType<T>;
    required?: boolean;
    default?: D | DefaultFactory<D>;
    validator?(value: unknown): boolean;
}
declare type DefaultFactory<T> = (props: Data) => T;
declare type Data = Record<string, unknown>;
export declare type PropType<T> = PropConstructor<T> | PropConstructor<T>[];
declare type PropConstructor<T = any> = { new (...args: any[]): T & {} };
export declare type Prop<T, D = T> = PropOptions<T, D> | PropType<T>;
export declare type ComponentPropsOptions<P = Data> = ComponentObjectPropsOptions<P> | string[];
export declare type ComponentObjectPropsOptions<P = Data> = { [K in keyof P]: Prop<P[K]> | null };

declare type InferPropType<T> =
  [T] extends [null] ? any 
: [T] extends [{ type: null | true }] ? any
: [T] extends [ObjectConstructor | { type: ObjectConstructor }] ? Record<string, any>
: [T] extends [BooleanConstructor | { type: BooleanConstructor }] ? boolean
: [T] extends [DateConstructor | { type: DateConstructor }] ? Date
: [T] extends [Prop<infer V, infer D>] ? (unknown extends V ? D : V)
: T;

export declare type ExtractPropTypes<O> =
  O extends object
  ? { [K in keyof O]: InferPropType<O[K]> }
  : { [K in string]: any };

export declare interface ComponentOptionsBase<Props> {
  setup?: (this: void, props: Props) => any;
}

export declare type ComponentOptionsWithObjectProps<PropsOptions = ComponentObjectPropsOptions, Props = ExtractPropTypes<PropsOptions>> 
  = ComponentOptionsBase<Props> & { props: PropsOptions };

export declare function defineComponent<PropsOptions extends ComponentPropsOptions>(options: ComponentOptionsWithObjectProps<PropsOptions>): any;

Playground

tony19
  • 125,647
  • 18
  • 229
  • 307
  • I think this might be the case https://github.com/microsoft/TypeScript/issues/43948 But his is my first gues, since example is pretty complicated – captain-yossarian from Ukraine Jun 27 '21 at 18:15
  • What are you trying to achieve? I believe `validator` method should infer argument type based on `type` property. I think using explicit type for `validator` argument is a mistake. THe fact that `validator` argument is infered to `unknown` probably is a mistake. This is only my opinion – captain-yossarian from Ukraine Jun 27 '21 at 18:22
  • @captain-yossarian Thanks for the issue link. That might be the problem here. – tony19 Jun 27 '21 at 19:19
  • @captain-yossarian The goal is for the `props` argument in `setup()` to be inferred from the previously declared `props` property. I agree there are probably some mistakes in the typing, and I'm interested in fixing that. – tony19 Jun 27 '21 at 19:24

2 Answers2

0

UPDATE

type Constructors =
  | BooleanConstructor
  | ObjectConstructor
  | StringConstructor


type DefaultFactory<T> = (props: Data) => T;
type Data = Record<string, unknown>;

type Entity<T extends Constructors> = {
  type: T,
  default: InstanceType<T> | DefaultFactory<T>
  validator: (value: ConstructorMappings<T>) => boolean
}

type ConstructorMappings<T extends Constructors> =
  T extends StringConstructor
  ? string
  : T extends BooleanConstructor
  ? boolean
  : T extends ObjectConstructor
  ? object
  : never;

 type MakeSetup<Props extends Record<string, Entity<Constructors>>>={
     [Prop in keyof Props]:ConstructorMappings<Props[Prop]['type']>
 } 

interface Props<T extends Constructors> {
  props: {
    size: Entity<T>
  },
  setup: (arg: MakeSetup<this['props']>) => any
}

export declare function defineComponent<C extends Constructors>(options: Props<C>): any;

defineComponent({
  props: {
    size: {
      type: String,
      default: 'lg',
      validator: (value /** infered as a string */) => true,
    }
  },
  setup:(props)=> {
    console.log(props.size.charAt(0)) // ok
  }
})


As you see, value argument is infered now.

  • Thanks for this. However, this is not quite what I need. The `props` property and the `props` argument are not supposed to be the same types. The `props` argument is created based on the declaration of the `props` property. Since `size` has a `type` of `String` (the constructor), the `props` argument should contain a `size` property of type `string` (the type). There are actually multiple ways to declare `props`, but that's probably beyond the scope of the question. – tony19 Jun 28 '21 at 04:59
  • For reference, `defineComponent`'s `setup()` is from [Vue's Composition API](https://v3.vuejs.org/guide/composition-api-setup.html#arguments). The `props` property is part of [Vue's Options API](https://v3.vuejs.org/api/options-data.html#props). – tony19 Jun 28 '21 at 05:15
  • I made an update. let me know if it works for you. However, I'd willing to bet that Vue should have a support for this – captain-yossarian from Ukraine Jun 28 '21 at 06:49
0

It turns out this is a design limitation of TypeScript, as explained by TypeScript's lead architect, Anders Hejlberg:

ahejlsberg commented on Jun 29, 2020

This is a design limitation. Similar to #38872. A arrow function with no parameters is not context sensitive, but a function expression with no parameters is context sensitive because of the implicit this parameter. Anything that is context sensitive is excluded from the first phase of type inference, which is the phase that determines the types we'll use for contextually typed parameters. So, in the original example, when the value for the a property is an arrow function, we succeed in making an inference for A before we assign a contextual type to the a parameter of b. But when the value is a function expression, we make no inferences and the a parameter is given type unknown.

tony19
  • 125,647
  • 18
  • 229
  • 307