Ошибка вывода типа параметра TypeScript - PullRequest
0 голосов
/ 05 июля 2018

Я создал для этого отчет об ошибке , но, возможно, у кого-то есть идея для обхода проблемы. По сути, я хочу иметь некоторые аргументы для функции, условно основанные на типе другой:

declare function foo<P>(params: { f: (p: P) => void } & P): void;

foo({
  f(params: { bar(x: number): void }) {},
  bar(x) {},
});

Таким образом, существует специальный параметр f, который имеет тип функции, и любые свойства, которые ожидает эта функция, должны быть включены в качестве аргументов foo. Здесь снова параметр к bar выводится как any, но должен быть number.

Ответы [ 2 ]

0 голосов
/ 15 июля 2018

Как мы обсуждали в комментариях, сценарий фактически связан с реагирующими компонентами и тем, как новая функция универсального компонента взаимодействует с общими параметрами по умолчанию.

Проблема

Если у нас есть компонент реагирования, у которого есть универсальный параметр, а значение по умолчанию для универсального параметра имеет поле функции, то параметры функции, которую мы передаем, не будут выведены. Простой пример будет:

  class Foo<P = { fn : (e: string)=> void }> extends React.Component<P>{}
  let foo = () => (
    <div>
      <Foo fn={e=> e}/> {/* e is any */}
    </div>
  )

Возможное решение

Позвольте мне предвосхитить это с оговоркой, я не знаю, это работает во всех ситуациях или зависит ли это от поведения компилятора, которое изменится. Что я обнаружил, так это то, что это очень хрупкое решение, изменив любую деталь, даже с чем-то, что можно было бы разумно считать эквивалентным, и оно не будет работать. Что я могу сказать, так это то, что, как я представил, это сработало для того, на чем я его тестировал.

Решение состоит в том, чтобы воспользоваться тем фактом, что типы свойств берутся из возвращаемого значения конструктора, и если мы немного упростим вещи для универсального параметра по умолчанию, мы получим нужный нам вывод. Единственной проблемой является обнаружение того, что мы имеем дело с универсальным параметром по умолчанию. Мы можем сделать это, добавив дополнительное поле к параметру по умолчанию, а затем протестировать его, используя условные типы. Еще одно предостережение: это не работает для простых случаев, когда P может быть выведен на основании используемых свойств, поэтому у нас будет пример, когда компонент извлекает свойства из другого компонента (который фактически ближе к вашему варианту использования):

// Do not change { __default?:true } to DefaultGenericParameter it only works with the former for some reason
type IfDefaultParameter<C, Default, NonDefault> = C extends { __default?:true } ? Default : NonDefault
type DefaultGenericParameter = { __default? : true; }

export type InferredProps<C extends React.ReactType> =
  C extends React.ComponentType<infer P> ? P :
  C extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[C] :
  {};

// The props for Foo, will extract props from the generic parameter passed in 
type FooProps<T extends React.ReactType = React.ComponentClass<{ fn: (s: string )=> void }>> = InferredProps<T> & {
  other?: string
}

// Actual component class
class _Foo<P> extends React.Component<P>{}

// The public facing constructor 
const Foo : {
  new <P extends React.ReactType & {__default?: true} = React.ComponentClass<{ fn: (s: string )=> void }> & DefaultGenericParameter>(p: P) :
  IfDefaultParameter<P, _Foo<FooProps>, _Foo<FooProps<P>>>
} = _Foo as any;

  let foo = () => (
    <div>
      <Foo fn={e=> e}/> {/* e is string */}
      <Foo<React.ComponentClass<{ fn: (s: number )=> void }>> fn={e=> e}/> {/* e is number */}
    </div>
  )

Применяется в материале UI

Один пример, который я попробовал в репозитории Material-Ui, - это Avatar компонент, и он, кажется, работает:

export type AvatarProps<C extends AnyComponent = AnyComponent> = StandardProps<
  PassthruProps<C, 'div'>,
  AvatarClassKey
> & {
  alt?: string;
  childrenClassName?: string;
  component?: C;
  imgProps?: React.HtmlHTMLAttributes<HTMLImageElement>;
  sizes?: string;
  src?: string;
  srcSet?: string;
};

type IfDefaultParameter<C, Default, NonDefault> = C extends { __default?:true } ? Default : NonDefault
type DefaultGenericParameter = { __default? : true; }

declare const Avatar : {
  new <C extends AnyComponent & DefaultGenericParameter = 'div' & DefaultGenericParameter>(props: C) 
  : IfDefaultParameter<C, React.Component<AvatarProps<'div'>>, React.Component<AvatarProps<C>>>
}

Использование

const AvatarTest = () => (
  <div>
    {/* e is React.MouseEvent<HTMLDivElement>*/}
    <Avatar onClick={e => log(e)} alt="Image Alt" src="example.jpg" />
    {/* e is {/* e is React.MouseEvent<HTMLButtonElement>*/}*/}
    <Avatar<'button'> onClick={e => log(e)} component="button" alt="Image Alt" src="example.jpg" />
    <Avatar<React.SFC<{ x: number }>>
      component={props => <div>{props.x}</div>} // props is props: {x: number;} & {children?: React.ReactNode;}
      x={3} // ok 
      // IS NOT allowed:
      // onClick={e => log(e)}
      alt="Image Alt"
      src="example.jpg"
    />
  </div>

Удачи в этом, дайте мне знать, помогло ли это. Это была забавная маленькая проблема, которая не давала мне уснуть в течение 10 дней:)

0 голосов
/ 05 июля 2018

Всякий раз, когда я вижу требование иметь тип функции, который зависит от значений аргумента, первое, что приходит на ум: «использовать перегрузки»:

declare function foo(params: { tag: 'a', bar(x: number): void }): void;
declare function foo(params: { tag: 'b', bar(x: boolean): void }): void;

foo({ tag: 'a', bar(x) { console.log('x =', x)} }); // (parameter) x: number

Обновление Я предполагаю, что в следующем коде foo и f являются компонентами React, а foo предоставляет f в качестве его свойства, а также все свойства, которые f есть.

declare function foo<P>(params: { f: (p: P) => void } & P): void;

foo({
  f(params: { bar(x: number): void }) {},
  bar(x) {},
});

Существует способ переписать его, если вы не настаиваете на том, чтобы имя f (которое является свойством foo), а также типы f свойств были объявлены встроенными в foo аргументов при вызове.

Вы можете объявить тип, описывающий свойства такого составного компонента, он принимает два общих параметра: тип свойств внутреннего компонента и имя внутреннего компонента во внешних свойствах:

type ComposedProps<InnerProps, N extends string> = 
       InnerProps & { [n in N]: (props: InnerProps) => void };  

// Also I assume in reality it does not return `void` but `React.Element`,
//  but I don't think it matters

declare function foo<P>(params: P): void;

// foo generic parameter is given explicitly,
//  which allows to infer (and not repeat) parameter types for `f` and `bar` 
foo<ComposedProps<{bar(x: number): void}, 'f'>>({
  f(params) {}, // params: { bar(x: number): void; }
  bar(x) {}, // (parameter) x: number
});
...