Pesquisa de site

reactjs - Como obter os tipos reais dos argumentos de uma função passada para outra função


Eu tenho um hook customizado no React que realiza alguns efeitos colaterais para fazer requisições fetch.

Em seu núcleo, o hook aceita uma função de busca e retorna uma versão decorada para ser chamada pelo componente. Quando a função decorada é chamada, ela define a resposta no estado e também a retorna (estrutura de retorno semelhante ao useState).

A função fetch passada e a função decorada terão exatamente os mesmos parâmetros/tipos, mas o tipo de retorno será diferente.

O problema que estou enfrentando é na adição de tipos tanto aos dados de estado retornados quanto à função decorada retornada. Parece que só consigo fazer um ou outro funcionar sem forçar o usuário do hook a fornecer o que acredito ser dados duplicados.

// Dummy useState for dep-free example
function useState<State>(initial: State): [State, (newState: State) => void] {
    return [initial, (newState: State) => {}];
}

function useFetch<Data, FetchParams extends any[]>(
    fetchFunction: (...args: FetchParams) => Promise<Response>, 
    initial_value: Data | null = null
): [Data | null, (...args: FetchParams) => Promise<void>] {
    const [response, setResponse] = useState<Data | null>(initial_value);

    async function wrappedFetch(...args: FetchParams): Promise<void> {
        // Side Effects
        const res = await fetchFunction(...args);
        const result = await res.json();

        // Side Effects

        if (res.ok) {
            setResponse(result);
        }
        // Side Effects
    }

    return [
        response,
        wrappedFetch
    ];
}

No uso, se você fornecer o tipo para o estado retornado, você também deve fornecer os tipos dos parâmetros, o que eu não gosto. Usar o Parameters do TypeScript facilita isso, mas parece desnecessário. Esse hook será usado centenas de vezes na base de código, e eu não quero forçar os desenvolvedores a passar o que parece ser dados duplicados toda vez.

async function getStuff(value: string): Promise<Response> {
    return new Promise((res) => {
        res(new Response(value))
    })
}

interface SampleData {
    value: 'string';
}

const [test, getTest] = useFetch<SampleData, Parameters<typeof getStuff>>(getStuff)

getTest('test'); // Correctly typed, passing nothing or a non-string will error.

Se você não passar nenhum parâmetro de tipo para useFetch, ele inferirá o tipo correto da função, mas não tem como saber o tipo de test.

Objetivo

Quero poder usar o hook com o tipo correto associado tanto a test quanto a getTest, sem precisar fornecer os argumentos - assim:

const [test, getTest] = useFetch<SampleData>(getStuff)
// test = SampleData | null
// getTest = (value: string) => Promise<void>

O que mais eu tentei

  1. Usando Parameters dentro do hook:
async function wrappedFetch(...args: Parameters<typeof fetchFunction>): Promise<void> {

Mas isso parece usar o tipo padrão fornecido (any[]) em vez de ser baseado na função real passada.

  1. Fornecendo um valor padrão para o FetchParams para que o desenvolvedor possa passar o tipo Data sem necessidade de FetchParams:
function useFetch<Data, FetchParams extends any[] = any[]>(

Mas fornecer um valor padrão impede que qualquer inferência de tipo aconteça, tornando o FetchParams inútil, e voltamos a forçar o desenvolvedor a passar os argumentos toda vez, apenas sem segurança de erro/tipo se ele optar por não fazê-lo.


Link para o TypeScript Playground