Why does regular function break type inference?

Viewed 143

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

2 Answers

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.

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.

Related