Búsqueda de sitios web

Problema de inferencia de tipos en TypeScript


Estoy usando TypeScript con el controlador de MongoDB para node.js. Nota, esta no es una pregunta sobre Mongo, es solo el caso particular de uso de este problema que estoy teniendo.

Prácticamente todas las llamadas a Mongo utilizan un patrón de (arg1, arg2, ..., argn, callback) donde callback es una función que acepta (err, res). Sin embargo, quiero usar promesas. Estoy intentando simplificar mi uso escribiendo una función envolvente de ayuda, de la siguiente manera:

function mongoWrap<T>(action: (callback: (err: Error, res: T) => void) => void) : q.IPromise<T>
{
    var promise = q.Promise<T>((resolve, reject) => {
       action((err, res) => {
           if (err) reject(err);
           else resolve(res);
       });
    });

    return promise;
}

Esto funciona muy bien, excepto que por alguna razón el compilador no puede inferir el tipo de T. Tengo que hacer llamadas como:

var dbPromise = mongoWrap<mongodb.Db>(cb => mongoClient.connect("mongodb://localhost:27017/database", cb));

Si omito la parte <mongodb.Db>, el resultado es Promise<{}> y pierdo la seguridad de tipos. Sin embargo, el compilador sabe claramente que el argumento callback de la llamada a Mongo es (err: Error, db: Db) => void.

¿Qué puedo hacer para que el compilador infiera correctamente el tipo de T?

Respuesta #1

Typescript es capaz de inferir el tipo de algunas funciones genéricas, pero tiene algunas limitaciones.

Dado que no hay información en la sección genérica del manual, decidí hacer algunas pruebas y ver dónde falla.

  1. una función simple que toma 1 parámetro.
function genericFunction<T>(value: T): T {
    return value;
}

// type of val is Window
let val = genericFunction(window); 

Esto funciona, no hay necesidad de especificar el tipo de T manualmente.

  1. una función con 2 parámetros genéricos.
function genericFunction2<T>(value: T, anotherValue: T) : T {
    return value;
}

// type of val is String
let val = genericFunction2("b", "5"); 

// compilation error type of T can't be inferred from usage
let anotherVal = genericFunction2("b", 5); 

Esto funciona, no hay necesidad de especificar el tipo de T manualmente.

  1. una función que recibe un callback y un valor.
function callBackAndValue<T>(action: (value: T) => T, value: T): T {
    return action(value);
}

// type of val is string
let val = callBackAndValue((value: string) => value + "5", "abc "); 

Esto funciona, no hay necesidad de especificar el tipo de T manualmente.

  1. una función que recibe un callback y un valor pero devuelve una promesa.
function callBackAndValueWithPromise<T>(action: (value: T) => T, value: T): Promise<T> {
    return new Promise<T>((resolve, reject) => {
        resolve(action(value));
    });
}

// type of val is Promise<string>
let val = callBackAndValueWithPromise((value: string) => value + "5", "abc "); 

Esto funciona, no hay necesidad de especificar el tipo de T manualmente.

  1. una función que recibe únicamente una función de T a T
function onlyCallback<T>(action: () => T) : T {
    return action();
}

// type of val is string
let val = onlyCallback(()=> "abc"); 

Esto funciona, no hay necesidad de especificar el tipo de T manualmente.

  1. Una función que recibe una función de nada a T que devuelve una promesa.
function onlyCallbackWithPromise<T>(action: () => T): Promise<T> {
    return new Promise<T>((resolve, reject) => { 
        resolve(action());
    });
}

// the type of val is Promise<string>
let val = onlyCallbackWithPromise(()=> "abc"); 

Esto funciona, no hay necesidad de especificar el tipo de T manualmente.

  1. Una función que recibe una función que toma una función. El caso en la pregunta.
function typeFromCallbackOfCallback<T>(action: (callback: (value: T) => void) => void): Promise<T> {
    return new Promise<T>((resolve, reject) => {
        action((value) => {
            resolve(value);
        });
    });
}

// here the compiler fails to infer the type cb should take as a parameter and it seems to default to object({})
// type of Val is Promise<{}>
let val = typeFromCallbackOfCallback(cb => cb("abc")); 

Esto ya no funciona y necesita que se especifique el tipo manualmente.

Dado que el compilador está limitado en este momento, supongo que estás obligado a tener que especificar el tipo en este caso. Esa es la solución que también se da en el manual para los casos en los que la inferencia de tipos falla.

Agregar otro parámetro del tipo T lo arregla, pero no coincide exactamente con tu caso.

function lastOne<T>(action: (callback: (value: T) => void) => void, b: T): Promise<T> {
    return new Promise<T>((resolve, reject) => {
        action((value) => {
            resolve(value);
        });
    });
}

// type of var is Promise<string>
let var = lastOne(cb => cb("abc"), "a");

Esto funciona, no hay necesidad de especificar el tipo de T manualmente.

Respuesta #2

Me equivoqué, esto tampoco funciona en C#. Aquí está el ejemplo del mismo código de TypeScript, escrito en C#, que no funciona:

using System.IO;
using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        Console.WriteLine("Hello, World!");
        var task = Wrapper(cb => FakeMongoFunctionWithCallback(5, cb));
    }

    static void FakeMongoFunctionWithCallback(int arg, Action<Exception, int> callback)
    {
        // just pass through the arg into the callback, pretending we went to a db and came back with that result
        callback(null, arg);
    }

    static Task<T> Wrapper<T>(Action<Action<Exception, T>> action) {
        var tcs = new TaskCompletionSource<T>();
        action((err, res) => {
            if (err != null) tcs.SetException(err);
            else tcs.SetResult(res);
        });

        return tcs.Task;
    } 
}

Artículos relacionados