Skip to content
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

Define assignability relation for primitive-constrained type parameters #2694

Open
RyanCavanaugh opened this issue Apr 9, 2015 · 7 comments
Labels
Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@RyanCavanaugh
Copy link
Member

The spec does not define any case in Assignment Compatibility where S is a type parameter constrained to a primitive type and T is that same primitive type.

This leads to some weird errors, e.g. T is not assignable to number even though T extends number:

enum E { A, B, C }
enum F { X, Y, Z }

function f<T extends number>(x: T[]): T {
    var y: number = x[0]; // Error, T is not assignable to number...??
} 

// f<T extends number> is useful:
var x = f([E.A, E.B]); // ok, x: E
var y = f([F.X, F.Y]); // ok, y: F
var z = f(['foo', 'bar']); // Error

See also http://stackoverflow.com/questions/29545216/inconsistent-behavior-of-enum-as-constrained-parameter-in-generic

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Apr 9, 2015
@dsherret
Copy link
Contributor

If primitive types aren't allowed to be extended, then how is writing T extends number useful and why should it be allowed? It doesn't seem to constrain anything in the language.

Someone could always write the above function this way, which I think makes more sense:

function f(x: number[]) {
    var y: number = x[0];
} 

Or maybe I'm missing something?

@RyanCavanaugh
Copy link
Member Author

If primitive types aren't allowed to be extended, then how is writing T extends number useful and why should it be allowed?

See the examples var x, var y, and var z in the original post. Enum members are subtypes of number, and it's useful to chain the type of the input of the function to its output while also restricting what kinds of types the function can handle.

@dsherret
Copy link
Contributor

Ok, that makes sense.

Btw, it might be beneficial to add the ability to constrain by enum or an EnumMember.

@jeffreymorlan
Copy link
Contributor

It's not only primitive types where the lack of assignability of a type parameter to its constraint hurts; I've also run into this with unions. Here's a real example of that:

interface ComparableObject { compare(other: ComparableObject): number; }
type Comparable = number | string | ComparableObject;

function compare(a: Comparable, b: Comparable): number {
    // ...
}

function binarySearch<T extends Comparable>(array: T[], element: T) {
    // ...
    compare(array[i], element) // Error: T not assignable to Comparable
    // ...
}

It gets even worse with class hierarchies:

class Column<T extends Comparable> {
    getValue(row: number): T { return; } // abstract
    render(value: T, node: HTMLElement) {} // abstract
    sort(rows: number[]) {
        // More errors here...
        return rows.sort((a, b) => compare(this.getValue(a), this.getValue(b)));
    }
}

// Yet another error here - and no way to hack around it with casts!
class FancyColumn<T extends Comparable> extends Column<T> { /* ... */ }

I don't understand why subtyping/assignability of type parameters needs to have a separate rule for every kind of constraint (object, primitive, union, another type parameter, ...) - seems that just one general rule should suffice: "S is a type parameter, and S's constraint is (a subtype of|assignable to) T."

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this and removed In Discussion Not yet reached consensus labels May 4, 2015
@RyanCavanaugh RyanCavanaugh added this to the Community milestone May 4, 2015
@RyanCavanaugh
Copy link
Member Author

Approved

@JsonFreeman
Copy link
Contributor

@jeffreymorlan the unions issue is fixed by #2778. But as you mention, it does seem silly that every kind of constraint has to be accounted for. Probably the more correct thing to do is to recursively check if the base constraint of S is assignable to T

@mkantor
Copy link
Contributor

mkantor commented Dec 6, 2024

I stumbled across this ancient issue and if I'm understanding it correctly I think it can be closed. These days code like this legal:

function f<T extends number>(x: T) {
  const y: number = x // no error
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants