microsoft / microsoft/TypeScript

Type Guards and CallBack Parameters are any, if worked, could successfully defined a callback type and respective guard

Open
#27,531 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Discussion
Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
2d 4h
Merged PRs (30d)
132

Description

TypeScript Version: typescript@3.2.0-dev.20181003

Two bugs in one, if both of these issues work, I would be able to properly defined a callback type function call signature overloads and a type guard to go with it.

CallBack, Type Guards with extends fails, CallBack with overloads signatures types become any type, instead of union of their respective params.

Issues

  1. Callback types params with methods overloading are always any type, should be respective union of each parameters types.
  2. Type Guards for more complex type guards doesn't work.

Summary

If all of this were to work, I would be able to correct write a typescript type for a callback with its associated type guard.

1. CallbackTypes are not correctly computed, which expected to be a union of the respective overloads params.
// function call signatures.
interface CallBack<T>
{
    () : void
    (error : Error) : void
    (error : undefined, result : T) : void
    (error : null, result : T) : void
}

function takeCallBack(callback: CallBack<number>)
{
    callback();
    callback(null, {A:77});
    callback(new Error(''));
}

//Fails to determine which function signature was used to call, because it could have been any of the //formats. The final format is an any type. It would be nice to  be able to write a type guard


//[ts] 'result' is declared but its value is never read.
//[ts] Argument of type '(error: any, result: any) => void' is not assignable to parameter of type 'CallBack<number>'
takeCallBack((error, result) => {

});

Expected, however, this is not enough information us to write a proper type guards
and we have also lost information about the results, which is the most important.
we need to be able to distinguish between T and T | undefined.

takeCallBack((error : Error | undefined | null, result : T | undefined) => {

}); 

CallBack type formulation

The only was to be able to do this, without having complicate solution, by means on inspection the body of the the function in which the callback is call to determine or infer the variants. Would be to break the callback definition into three.

Callback A:
interface CallBackError<T>
{
    () : void   // sucessfully, results undefined
    (error : Error) : void  // Error
}

function takeCallBackNoResults(callback: CallBackError<number>)
{
    callback();
    callback(new Error(''));
}

//[ts] 'results' is declared but its value is never read.
//[ts] Argument of type '(error: any, results: any) => void' is not assignable to parameter of type 'CallBackError<number>'.
takeCallBackNoResults((error,results) => {

});

takeCallBackNoResults((error : Error,results : undefined) => {

});

I can now write a guard statement for this, to ensure a clean output of results, however, in this case I shouldn't need it.

CallBack B:

Here I can use the null permutation of error, to determine

interface CallBackResult<T> 
{
    (error : null, res : T) : void 
    (error : Error) : void
}
function takeCallBackResults(callback: CallBackResult<number>)
{
    callback(null, 6);
    callback(new Error(''));
}

//[ts] 'results' is declared but its value is never read.
//[ts] Argument of type '(error: any, results: any) => void' is not assignable to parameter of type 'CallBackResult<number>'.
takeCallBackResults((error,results) => {

});

takeCallBackResults((error : Error | null,results : number | undefined) => {

});

I can't write a guard for this, but would be nice if I didn't need it in the first place, but there is now getting around that.

Callback C:
interface CallBackOpResult<T> 
{
    () : void
    (error : null, res : T) : void 
    (error : Error) : void
}
function takeCallBackOpResults(callback: CallBackOpResults<number>)
{
    callback();
    callback(null, 5);
    callback(new Error(''));
}

//[ts] 'results' is declared but its value is never read.
//[ts] Argument of type '(error: any, results: any) => void' is not assignable to parameter of type 'CallBackError<number>'.
takeCallBackOpResults((error,results) => {

});

takeCallBackNoResults((error : Error | null | undefined,results : number | undefined) => {

});

I can't write a guard for this, but would be nice if I didn't need it in the first place, but there is now getting around that. Because we have to now do an unnecessary check for undefined, when results mayn't have support undefined in the first place.

CallBack - Conclusion on how to write the final form

What we are not able to distinguish here is that, is whether results would be a plain T or meant to tbe T | undefined. This is a problem, the only way we able to to really know that is if error and results parameters could be treated as one and a set. I can write a type gaud, have knowledge of this already, to check for the 3 cases, but I will always have to check for undefined
What I could do is use the fact that error being of type null, means that results shouldn't have the undefined, I could strip it, but I wouldn't be able know difference of which results still
If I decided to not allow undefined error, for default case of no results I could be able to write a gaud check that would work, based on the input of error being null or not.

interface CallBackOpResult<T> 
{
    () // error = undefined, results = undefined
    (error : null, res : T) : void 
    (error : Error) : void
}

function takeCallBackOPResults(callback: CallBackOpResults<number>)
{
    callback();
    callback(null, 5);
    callback(new Error(''));
}

//[ts] 'results' is declared but its value is never read.
//[ts] Argument of type '(error: any, results: any) => void' is not assignable to parameter of type 'CallBackOpResults<number>'.
takeCallBackOpResults((error,results) => {

});
Simple Use of Type Guard:
takeCallBackNoResults((error : Error | null, results : number) => {
    
    if(!CallBackHasResults(error, results))
        console.log(error);// handle error
    else
    {
        results
    }
});
Type Guards Failed:

function CallBackHasResults1<Err extends undefined | null | Error, Result>(error : Err, results : Result) : results is undefined | Result
function CallBackHasResults1<Err extends null | Error, Result>(error : Err, results : Result) : results is Result
{
    return error !== null && error !== undefined;
    //return results;
}

// Fails to work correctly:
const callBack = (error : Error | null | undefined, results : number |undefined) => {
    
    if(!CallBackHasResults1(error, results))
        console.log(error);// handle error
    else
    {
        results // expect type = number | undefined
    }
}

// Fails to work correctly.
const callBack2 = (error : Error | null, results : number | undefined) => {
    
    if(!CallBackHasResults1(error, results))
        console.log(error);// handle error
    else
    {
        results // expect type = number
    }
}

The alternative would be to introduce a compound type for callbacks, were by both error and Results are used to dictate the output format, which I have achieved here by doing this manual.

Another alternative, using the this parameter, which may work if the overloads function parameters unioned correctly.
interface CallBackOpResult2<T> 
{
    () // error = undefined, results = undefined
    (error : null, res : T) : void 
    (error : Error) : void
}

interface TT<T> {
    callback : CallBackOpResult2<T>
}

function takeCallBackResults3(callback: TT<number>['callback'])
{
    //this = {a : 'a'}
    callback();
    callback(null, 5);
    callback(new Error(''));
}

//[ts] 'results' is declared but its value is never read.
//[ts] Argument of type '(error: any, results: any) => void' is not assignable to parameter of type 'CallBackError<number>'.
takeCallBackResults3((error,results) => {

});

takeCallBackNoResults3(function (error, results) {
    
    
    if(!CallBackHasResults3(error, results))
        console.log(error);// handle error
    else
    {
        results
    }
});


function CallBackHasResults3<T extends {callback:any},Err extends undefined | null | Error, Result>(this : T, error : Err, results : Result) : results is undefined | Result
//function CallBackHasResults3<Err extends null | Error, Result>(this : {a:'a'}, error : Err, results : Result) : results is Result
{
    return error !== null && error !== undefined;
    //return results;
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the callback overload and generic type-guard examples in the issue using the reported TypeScript 3.2.0-dev.20181003 behavior. Reduce them to separate reproductions for overload parameter inference and type-guard narrowing; done means the examples no longer infer callback parameters as any and the stated result types narrow as expected.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.