Правильная специализация типа - PullRequest
0 голосов
/ 15 апреля 2020

Я написал перегруженную функцию, как в этом CR вопросе , и она работает, кроме набора текста.

Существуют специальные вспомогательные методы, такие как

function booleanAgg(
    value: boolean,
    setter: Dispatch<SetStateAction<boolean>>,
    makeSetter: (value: boolean) => ((event?: any) => void)) {
    ...
}

в основном методе

export function useStateEx<S extends boolean|string|number>(initialState: S) : unknown {
    ...
    if (typeof value === 'boolean') {
        // @ts-ignore
        return [value, setter, booleanAgg(value, setter, makeSetter)]
    ...
}

Очевидно, что машинопись видит, что value является логическим значением, и сообщает (S & false) | (S & true), что точно AFAIK S & boolean. С более сложными типами это не работает, так как сообщение об ошибке гласит:

Argument of type 'Dispatch<SetStateAction<S>>' is not assignable to parameter of type 'Dispatch<SetStateAction<boolean>>'.
  Type 'SetStateAction<boolean>' is not assignable to type 'SetStateAction<S>'.
    Type 'false' is not assignable to type 'SetStateAction<S>'.ts(2345)

С @ts-ignore это работает хорошо, но как я могу получить правильные типы?

Минимальный воспроизводимый пример

import { useState } from 'react';

function booleanAgg(
    value: boolean,
    setter: (value: boolean) => void,
    makeSetter: (value: boolean) => ((event?: any) => void)) {
    throw new Error('irrelevant');
}

export function useStateEx<S extends boolean|string|number>(initialState: S) : unknown {
    const [value, setter]: [S, (v: S) => void] = useState(initialState);
    function makeSetter(value: S) : ((value: boolean) => ((event?: any) => void)) {
        throw new Error('irrelevant');
    }
    if (typeof value === 'boolean') {
        // @ts-ignore
        return [value, setter, booleanAgg(value, setter, makeSetter)];
    } else {
        throw new Error('irrelevant');
    }
}

Новое сообщение об ошибке

После адаптации примера, как предлагается в комментариях, сообщение об ошибке изменилось:

Argument of type '(v: S) => void' is not assignable to parameter of type '(value: boolean) => void'.
  Types of parameters 'v' and 'value' are incompatible.
    Type 'boolean' is not assignable to type 'S'.
      'boolean' is assignable to the constraint of type 'S', but 'S' could be instantiated with a different subtype of constraint 'string | number | boolean'.ts(2345)

1 Ответ

0 голосов
/ 16 апреля 2020

Я мог бы частично решить проблему, добавив больше дженериков. Чтобы код компилировался без реакции, замените импорт следующими объявлениями:

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);

Полный код:

import { useState, Dispatch, SetStateAction } from 'react';

interface BaseAgg<S extends boolean|string|number> {
    value: S
    setter: (value: S) => void
}

interface BooleanAgg extends BaseAgg<boolean> {
    falseHandler: (event?: any) => void,
    trueHandler: (event?: any) => void,
    toggleHandler: (event?: any) => void,
}

interface NormalAgg<S extends string|number> extends BaseAgg<S> {
    makeSetter: (value: S) => ((event?: any) => void)
    changeHandler: (e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) => void
}

function booleanAgg(value: boolean, setter: (value: boolean) => void, makeSetter: (value: boolean) => ((event?: any) => void)) : BooleanAgg {
    return {
        value,
        setter,
        falseHandler: makeSetter(false),
        trueHandler: makeSetter(true),
        toggleHandler: makeSetter(!value),
    };
}

function stringAgg<S extends string>(value: S, setter: (value: S) => void, makeSetter: (value: S) => ((event?: any) => void)) : NormalAgg<S> {
    return {
        value,
        setter,
        makeSetter,
        changeHandler: (e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) => {
            e.stopPropagation();
            setter(e.currentTarget.value as S);
        }
    };
}

function numberAgg<S extends number>(value: S, setter: (value: S) => void, makeSetter: (value: S) => ((event?: any) => void)) : NormalAgg<S> {
    return {
        value,
        setter,
        makeSetter,
        changeHandler: (e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) => {
            e.stopPropagation();
            setter(+e.currentTarget.value as S);
        }
    };
}

/**
 * Works like useState and adds a third element containing boolean-specific handlers directly useable in buttons and checkboxes.
 */
export function useStateEx<S extends boolean>(initialState: S) : [S, Dispatch<SetStateAction<S>>, BooleanAgg];

/**
 * Works like useState and adds a third element containing string-specific handlers directly useable in text fields.
 */
export function useStateEx<S extends string>(initialState: S) : [S, Dispatch<SetStateAction<S>>, NormalAgg<S>];

/**
 * Works like useState and adds a third element containing number-specific handlers directly useable in text fields.
 */
export function useStateEx<S extends number>(initialState: S) : [S, Dispatch<SetStateAction<S>>, NormalAgg<S>];

/**
 * Prohibits use with any type not handled in the above overloads.
 */
export function useStateEx(initialState: any) : never;

export function useStateEx<S extends boolean|string|number>(initialState: S) : unknown {
    const [value, setter] = useState(initialState);
    function makeSetter(value: S) {
        return function(e: any) {
            if (typeof e?.stopPropagation === 'function') e.stopPropagation();
            setter(value);
        };
    }
    switch (typeof value) {
    case 'boolean': return [value, setter, booleanAgg(value, setter as any, makeSetter as any)];
    case 'string': return [value, setter, stringAgg(value, setter, makeSetter)];
    case 'number': return [value, setter, numberAgg(value, setter, makeSetter)];
    }
}

Я не добавил обобщений для логического случая, поскольку я не хочу иметь дело с S extends boolean - тип, подобный false с его единственным значением, нельзя использовать в качестве состояния. Поэтому аргумент generi c, который я хотел бы использовать, находится в псевдокоде S=boolean || S extends number|string, который AFAIK не может быть выражен.

...