为什么常规函数会破坏类型推断?

Why does regular function break type inference?

我有一个 defineComponent() 函数,它接受一个对象作为参数。该对象可以包含一个 props 属性 和一个 setup() 属性,它接收一个 props 参数,其类型是从 props 属性 早前在对象中声明。

示例:

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

奇怪的是,如果 props.size.validator 声明为常规函数 (validator: function(value: string) { return true }) 而不是箭头函数,则 props 参数的推断类型是完全错误的。对于上面的例子。应该是:

{
  size: string
}

但是是这样的:

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

常规函数与工作代码中的箭头函数基本相同,所以我没想到它会破坏类型推断。

为什么常规函数会破坏 setup()props 参数的类型推断?


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

更新

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
  }
})


如您所见,现在推断出 value 参数。

事实证明这是 TypeScript 的设计限制,因为 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.