Skip to content

Allow narrowing type based on others properties when creating a new object #37224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
5 tasks done
macabeus opened this issue Mar 5, 2020 · 3 comments
Open
5 tasks done
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@macabeus
Copy link

macabeus commented Mar 5, 2020

Search Terms

  • narrowing type

Suggestion

Currently, TypeScript isn't narrowing the type based on others properties that is defined while creating a new object. For example. Let's say the we have the following function:

type TParams = { kind: 'name', value: string } | { kind: 'age', value: number }
const example = (params: TParams) => {
  ...
}

So we can call it using codes like:

example({ kind: 'name', value: 'macabeus' })
example({ kind: 'age', value: 23 })

But isn't possible to compile code like:

let nameOrAge: 'name' | 'age' = 'name'

example({
    kind: nameOrAge,
    value: (nameOrAge === 'name' ? 'macabeus' : 23),
})

Playground

but we know that, if nameOrAge is 'name', the property value will be a string, otherwise nameOrAge will be 'age' and value will be 23 - that is correct.
I think that would be useful if TS could check this behaviour.

Use Cases

It would be useful to reduce the amount of code on situations like described on the above section.
Currently, is necessary to write more lines to have the same behaviour:

let nameOrAge: 'name' | 'age' = 'name'

if (nameOrAge === 'name') {    
    example({
        kind: nameOrAge,
        value: 'macabeus',
    })
} else {
    example({
        kind: nameOrAge,
        value: 23,
    })
}

Playground

Related

I know that there are a lot of issues saying about narrowing types based on variables, such as:

  • that about narrowing on arrays values
  • that about narrowing when calling a function

But here I'm saying only about when is creating a new object, and as far as I know is a simpler case if compared on these others issues, since doesn't need to track the entire code base.
On the case that I'm saying, TS just need to check the others properties defined on the same object and if the variable name already is used. For example, TS doesn't need to change its behaviour on this case:

example({
  kind: nameOrAge,
  value: otherVariabledDeclaredOnOtherScope ? 'macabeus' : 23,
}) // still should fail

Probably it isn't the best solution, but I think that it follow the Pareto efficiency.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@jcalz
Copy link
Contributor

jcalz commented Mar 5, 2020

This is a particular use case of #12184 and similar to one of the motivating examples I had for #25051 (see this comment).

It seems that the suggestion here is that whenever a new object literal is created, the compiler should check to see if any union-typed value is mentioned multiple times, and then automatically distribute the control flow analysis over the different narrowings of that value. So if you have the following:

declare const x: A | B | C;
declare const f: <T>(t: T) => F<T>;
const obj = {x: x, fx: f(x)};

you'd want the compiler to notice that x is used multiple times inside obj, narrow x to A, B, and C in turn, compute the types for obj in each case, and then unite them into {x: A, fx: F<A>} | {x: B, fx: F<B>} | {x: C, fx: F<C>}.

I would love it if such things could happen automatically but I expect that the performance impact would be prohibitive, especially given the potential combinatoric nightmare of things like {x: x, y: y, z: z, fxyz: f(x, y, z)}.

In my fantasy world (aka #25051) I'd want to be able to manually ask the compiler to do such analysis so as to get this benefit while preserving the normal behavior and performance for normal code. In your case it would look something like

example({
    kind: nameOrAge,
    value: (nameOrAge === 'name' ? 'macabeus' : 23),
} as if switch(nameOrAge))

Oh well! :wistful-sigh:

@macabeus
Copy link
Author

macabeus commented Mar 5, 2020

@jcalz Thank you for reply!

It seems that the suggestion here is that whenever a new object literal is created

Yeah. The idea on this proposal is only when creating a new object literally, in order to avoid problems on performances and to be easer to implement.
I'm following the Pareto principle. I know that this isn't the best approach, but it's a very simpler.

the compiler should check to see if any union-typed value is mentioned multiple times, and then automatically distribute the control flow analysis over the different narrowings of that value

I don't know if is really necessary to automatically distribute for each value on the enum. For example, on this case is necessary to distribute just two times:

let nameOrSomething: 'name' | 'age' | 'height' | 'money'

example({
    kind: nameOrSomething,
    value: (nameOrSomething === 'name' ? 'macabeus' : 20),
})

Just need to check:

  • when nameOrSomething is 'name' and value is 'macabeus'. Is this object valid to use on example call?`
  • as well as when nameOrSomething is 'age' | 'height' | 'money' and value is 20. Is this object valid?

Are you looking other case that will demand to do more checks?

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed Too Complex An issue which adding support for may be too complex for the value it adds labels Mar 9, 2020
@RyanCavanaugh
Copy link
Member

The existence of tractable cases doesn't imply that combinatorially-explosive cases don't still exist. The algorithm can't be "look for the one comparison of the discriminant and then do the right thing".

If you wrote something like

let nameOrSomething: 'name' | 'age' | 'height' | 'money' | (30 other values)

example({
    kind1: nameOrSomething,
    kind2: nameOrSomething === "age" ? "age2" : nameOrSomething,
    value: (nameOrSomething === 'name' ? 'macabeus' : 20),
    value1: (nameOrSomething === 'age' ? nameOrSomething : 20),
    value2: (nameOrSomething === 'height' ? 'macabeus' : 20),
    value3: (nameOrSomething === 'money' ? 'macabeus' : nameOrSomething),
    value4: (nameOrSomething !== 'age' ? 'macabeus' : 20),
    value5: (nameOrSomething === 'money' && nameOrSomething === 'age' ? 'macabeus' : 20)
})

and then compared that to some complex definition of example it's not obvious to me how you'd avoid iterating the whole union many times over. This isn't an unrealistic scenario given the kinds of props people generate in JSX.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants