Skip to content

Discriminated union Key-Types #12448

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

Closed
stevekane opened this issue Nov 22, 2016 · 6 comments
Closed

Discriminated union Key-Types #12448

stevekane opened this issue Nov 22, 2016 · 6 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@stevekane
Copy link

stevekane commented Nov 22, 2016

TypeScript Version: 2.2.0-dev.20161122
Code

type GLUniform 
  = { tag: 'F', value: number }
  | { tag: 'F2', value: number[] }

function updateUniform<U extends GLUniform> ( u: U, t: U['value'] ) {
  switch ( u.tag ) {
    // update the value stored in GPU memory 
  }
}

updateUniform({ tag: 'F', value: 1 }, 2) // Compiled
updateUniform({ tag: 'F2', value: [ 1, 2 ] }, [ 3, 4 ]) // Compiled
updateUniform({ tag: 'F2', value: [ 1, 2 ] }, 5 ) // ): Compiled ( NOT expected )
updateUniform({ tag: 'F2', value: [ 1, 2 ], 'FooBar' ) // Did Not Compile ( as expected )

Expected behavior:
updateUniform({ tag: 'F2', value: [ 1, 2 ] }, 5 ) should fail to compile because the type of value for 'F2' is number[] and not number.

Actual behavior:
It seems that U['value'] is unioning all the possible values of value for all types in the discriminated union. I would expect it to understand that the value must match the type of "value" key for the given candidate value of GLType ( in this case, { tag: 'F2', value: number[] } )

@RyanCavanaugh
Copy link
Member

So what happens here is that we try to infer a type for U and fail because the value property and t parameters don't match. When inference like this fails, we try the call with the type parameter as the constraint (this is useful so that you can invoke f<T extends Animal>(a: T, b: T): T with Cow, Horse and get back Animal) and succeed with U = GLUniform, which means U['value'] is number | number[], and the number[] argument is assignable to one of those. The string call fails for the same reason - we fall back to the constraint type, but string isn't either number or number[].

What you want here is for the second parameter's use of U to ignored for the purposes of inference so that the other use of U fixes to the F2 variant rather than falling back to the constraint. I think there are some suggestions floating around on this (I can't find one at the moment) but feel free to log an explicitly-worded suggestion to that effect so it's easier to find.

An alternative typing which is better for callers but worse for the implementer is:

function updateUniform<V, T extends { tag: GLUniform['tag'] , value: V }> ( u: T, t: V ) {
  switch ( u.tag ) {
    // update the value stored in GPU memory 
  }
}

Luckily you can have your caller signature and implementation signature be different, so you can write this and get the best of both worlds:

function updateUniform<V, T extends { tag: GLUniform['tag'] , value: V }> ( u: T, t: V ) {
function updateUniform(u: GLUniform, v: GLUniform['value']) {
  // ...

@mhegazy
Copy link
Contributor

mhegazy commented Nov 22, 2016

This can be simplified to use overloads with no generic types as such:

type Tag1 = { tag: 'F', value: number };
type Tag2 = { tag: 'F2', value: number[] };
type GLUniform = Tag1 | Tag2;

declare function updateUniform(t: Tag1, v: Tag1["value"]);
declare function updateUniform(t: Tag2, v: Tag2["value"]);

but regardless, i think there is an issue with when the constraint is instantiated. i would have expected this to work in the same way:

declare function updateUniform<T extends GLUniform, V extends T['value']>(t: T, v: V);

since we would infer for T and V independently, then check the constraint at the end, and you should have got a error then for V not matching T["value"].

@mhegazy mhegazy added the In Discussion Not yet reached consensus label Nov 22, 2016
@RyanCavanaugh RyanCavanaugh added the Suggestion An idea for TypeScript label Nov 23, 2016
@stevekane
Copy link
Author

stevekane commented Nov 23, 2016

@RyanCavanaugh thanks for the help I was able to get your first option to work ( worse for the implementor ) but I'm a little unclear on your second comment. How do I specify these two sigs? One w/o a body an the implementor w/ a body?

@mhegazy Thanks very much for you followup. Your idea is precisely what I thought of when I realized that TS did have a mechanism for function overloading ( on discriminated union-members in this case ) but as you can see in the code below, I am probably missing something. When I try to compile this code it complains that the type number cannot be added to the type number | number[].

interface F { tag: 'F', value: number }
interface F2 { tag: 'F2', value: number[] }
type GLT = F | F2

function update ( u: F, v: F['value'] ): void
function update ( u: F2, v: F2['value'] ): void
function update ( u: GLT, v: GLT['value'] ) {
  switch ( u.tag ) {
    case 'F':  return console.log(u.value + v)
    case 'F2': return console.log(v)
    default:   const n: never = u
               return n
  }
}

update({ tag: 'F', value: 5 }, 3)
update({ tag: 'F2', value: [ 5 ] }, [ 2 ])
// update({ tag: 'F2', value: [ 5 ] }, 2)
// matches({ tag: 'F2', value: [ 5 ] }, 2)

If I relax the generic signature as follow:

function update ( u: GLT, v: any ) 

then it works as expected an gives access to number addition and vector concatenation respectively after switch-discrimination.

@mhegazy
Copy link
Contributor

mhegazy commented Nov 23, 2016

The issue is u and v in the implementation of update are independent. type GLT['value'] is shorthand for number | number[]. So narrowing u in the switch statement does not necessarily narrow v. Even with the change proposed in #12448 (comment), this would still not work. I do not think this is specific to generics, keyof or T[K]. for instance:

var u: GLT;
var v = u.value;

switch (u.tag) {
    case 'F': console.log(u.value + v) // u.value is number, but v is number|number[]
    case 'F2': console.log(v)
}

tracking aliases and narrowing them is not a simple thing to do.

@stevekane
Copy link
Author

I have started using class-based polymorphism and methods to solve this problem. It's a sort of poor-man's typeclass solution that allows me to constrain polymorphism to instances which are defined ( extensible and not limited to the weirdness of overloaded function signatures )

@RyanCavanaugh
Copy link
Member

Happy to report that this errors as expected now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants