microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.08k stars 12.49k forks source link

Compiler not automatically inferring not null of Object #41926

Closed ultd closed 3 years ago

ultd commented 3 years ago

TypeScript Version: 4.1.2

Search Terms: non-null, assertion, automatic, inferring

Code

type EitherOrNull<T extends object, S extends object> = T & { [K in keyof S]: null } | S & { [K in keyof T]: null }

function someFunc(): EitherOrNull<{ error: Error }, { value: number }> {
     // some functionality..
     return { error: new Error('nope'), value: null } 
     // or `return { error: null, value: 33 }`
}

const anotherFunc = (): Error | null => {
    const { error, value } = someFunc()
    if(error){
        return new Error('something went wrong!')
    }

    const numberStr1 = value.toString()
                    // ^ Error: Object is possibly 'null'.(2531)

    // this works though:
    const numberStr2 = value!.toString()

    return null
}

EitherOrNull type ensures that the return type of someFunc is either T & { [K in keyof S]: null } or S & { [K in keyof T]: null }. The TS compiler will not allow return type of T & S or { [K in keyof T]: null } & { [K in keyof S]: null }

Comments such as "just use non-null assertion operator" is not helpful as the type returned from this function is used in other code downstream therefore the return type is important to work properly.

Expected behavior: I expect the TS compiler to know that value is not null because if error was not null, the anotherFunc function would've returned the error and code execution would've ceased and therefore the code which uses value would be unreachable. It's only after error is deemed null by code is when the usage of value occurs at which point the TS compiler should know that the value property is not null.

Actual behavior: The TS compiler gives an error stating that value "is possibly 'null'" even though it's impossible as the return type of someFunc function ensures that it wouldn't be being that error is null. The TS compiler is making me use non-null assertion operator or checking if value is not null using an if statement.

Playground Link: TS-Playground

Related Issues: None

RyanCavanaugh commented 3 years ago

Control flow narrowing works based on looking for syntax that applies to the variable in question. In this example, nothing directly seems to influence value, so it has its unnarrowed type.

We don't have the performance budget to do the sort of whole-universe counterfactual analysis that would be required to correctly detect this pattern.

ultd commented 3 years ago

But consider this code:


type EitherOrNull<T extends object, S extends object> = T & { [K in keyof S]: null } | S & { [K in keyof T]: null }

function someFunc(): EitherOrNull<{ error: Error }, { value: number }> {
     // some functionality..
     return { error: new Error('nope'), value: null } 
     // or `return { error: null, value: 33 }`
}

const anotherFunc = (): Error | null => {
    const result = someFunc()
    if(result.error){
        return new Error('something went wrong!')
    }

    const numberStr1 = result.value.toString()
    // ^ but this works?

    return null
}

TS Playground

Being that this example works and the only difference is the destructuring of the object returned, it makes sense that the type inferred would be passed to the newly declared const variable.

Would there need to be anymore computing being that the types are already being inferred by the TS compiler?

IllusionMH commented 3 years ago

Looks similar to #39026 which considered duplicate of #12184

ultd commented 3 years ago

No progress made on that duplicate in 4 years. Any resources you could point me to for contributing? Any document explaining the repo would be really helpful. It seems this will not be taken care of unless a PR is made by myself or someone else that's having this specific problem.

Reasoning:

Consider this piece of Go code:

func someFunc() (error, int) {
    // ... some functionality
    return errors.New("nope"), 0
    // or return nil, 33
}

func anotherFunc() error {
    err, value := someFunc()
    if err != nil {
        return fmt.Errorf("Something went wrong!")
    }

    valueStr := strconv.Itoa(value)
    return nil
}

I know TS is not Go and Go isn't TS but we should be able to return some form of typed tuple that the compiler can infer types of using control flow analysis. Destructuring objects allows for a type of "tuple" but unfortunately lacks the compiler's ability to infer the types.

What about Tuple type like this:

export type EitherOrNullTuple<T, S> = [T, null] | [null, S]

function someFunc(): EitherOrNullTuple<Error, number> {
    // some functionality..
    return [new Error('nope'), null]
    // or `return [null, 33]`
}

const anotherFunc = (): Error | null => {
   const [error, value] = someFunc()
   if(error){
       return new Error('something went wrong!')
   }

   const numberStr1 = value.toString()
    // same Error: Object is possibly 'null'.ts(2531)

   return null
}

This still exhibits the same behavior as the previous example (TS-Playground). Destructuring the TS typed tuple doesn't allow for control flow analysis. This is a bug in my view being that this is a built in TS type.

Why not just use try-catch blocks? Try-catch blocks are a pain to work with and the idea of writing code that can throw an error in one place and could be caught (and handled) elsewhere is not a good pattern to me (I understand this is debatable and to each his/her own).

We should try to improve a language by learning from others and this is a good pattern TS should adopt or should at least support.

RyanCavanaugh commented 3 years ago

No progress made on that duplicate in 4 years. Any resources you could point me to for contributing? Any document explaining the repo would be really helpful. It seems this will not be taken care of unless a PR is made by myself or someone else that's having this specific problem.

You can start by reading CONTRIBUTING.md, and there are hundreds of PRs affecting control flow you can look at to see how changes there work. Looking forward to your PR!