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

Allow intersected numbers to be used in arithmetics & element access #4372

Closed
ivogabe opened this issue Aug 20, 2015 · 18 comments
Closed

Allow intersected numbers to be used in arithmetics & element access #4372

ivogabe opened this issue Aug 20, 2015 · 18 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@ivogabe
Copy link
Contributor

ivogabe commented Aug 20, 2015

I was experimenting with intersection types and primitives, and I discovered that when you create an intersection type with a number, you can't use it in arithmetics and element access:

type extendedNumber = number & { extended: any };
let a: extendedNumber;
let obj: any;

a + 1; // Operator '+' cannot be applied to types 'number & { extended: any }' and 'number'.
obj[a]; // An index expression argument must be of type 'string', 'number', 'symbol, or 'any'.

Though you can't create properties on numbers, this can be useful to add brands to numbers, like this:

type int = number & { __intBrand: void };
function int(value: number) {
  return <int> (value | 0);
}

Currently intersections are only allowed on these locations if all parts of the intersection are number or string, but that rule should only apply to union types. Suggestion: for intersection types (at least) one type should be number (or string) if it's used in arithmetics or element access.

I've implemented a fix in #4373.

@DanielRosenwasser
Copy link
Member

I was thinking about this just for the sake of things like units of measure. As I mentioned in your PR, I think the only open questions are what to do with number & string (which I'm glad you added test coverage for). Otherwise, it sounds like this would be the correct thing to do.

@DanielRosenwasser DanielRosenwasser added the Suggestion An idea for TypeScript label Aug 20, 2015
@mhegazy mhegazy added the In Discussion Not yet reached consensus label Aug 20, 2015
@RyanCavanaugh
Copy link
Member

Reading this

type extendedNumber = number & { extended: any };

I see this
image

We can't reason about this object. It doesn't exist. We shouldn't allow it to be used as if it were a three-tipped two-pronged thing for the sake of letting someone hack in branding.

@ahejlsberg
Copy link
Member

I would add that number & { __intBrand: void } makes for a pretty poor error reporting experience as that is the representation you'll actually see for the type in error messages. If we're going to support branded primitive types I'd sooner do as a proper feature.

@ivogabe
Copy link
Contributor Author

ivogabe commented Aug 21, 2015

I agree that it doesn't give the best experience. number & { __intBrand: void } is already supported, the only thing is that arithmetics and element access are not working with such types. As you can see in the PR, it's easy to fix it. Removing all support for primitives in intersections would be a step back in my opinion. Giving type aliases a name would probably be the easiest fix to get nicer error messages.

To give some background why I'm using this, I'm working on a compiler that compiles TypeScript to LLVM. It currently only works with a very small subset, but I'm working on expanding that subset. Currently all variables should be a primitive (int32, int64, etc), a pointer or a struct. Brands are the only way to see the difference between an int32 and an int64 at the moment (as far as I know). I hope to put a preview on GitHub soon.

Besides that, it is actually possible to create such object in JavaScript, by adding the property on the prototype:

Number.prototype.extended = true;
3..extended; // true

You could use this to check whether some methods are supported in the current environment:

type ES6String = string & { includes(str: string): boolean; repeat(count: number): string};
function isES6String(str: string) str is ES6String {
    return (<ES6String> str).includes !== undefined;
}

function repeat(str: string, count: number) {
  if (isES6String(str)) {
    return str.repeat(count);
  } else {
    // for (let i = 0...
  }
}

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 2, 2015

@jbondc Agree that it might look strange, but number & { intBrand: void; } is already supported in the dev builds. Because of the semantics of &, that type is assignable to a number. Disallowing element access and arithmetics is inconsistent with these assignability rules, as you cannot use a number & ... as a number, while number & ... is actually a subtype of number.

My example with strings is probably not the best one. My use case is branding numbers. In my project I need to distinguish different primitive numbers, like int32 and uint16. Set types would be a better solution to this, but they're not supported in TypeScript, it will probably require a lot work to implement that correctly. Branding with intersections is working, except for element access and arithmetics.

I'd be open for other suggestions to distinguish different number types, other than intersections. Should I give more background info on my project?

@jbondc
Copy link
Contributor

jbondc commented Sep 2, 2015

Short term, would this not work?

type int32 = number;
type int64 = number;
let i: int64 = 666;

Gives a 'nominal' looking type system for numbers, enough to use the type names to emit types.

Set types are a lot of work, but it would allow proper checking for things like:

let num: number = 1.2;
let foo: int32 = num; // ? probably don't want this

Though haven't played around with intersection types.

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 2, 2015

As far as I know the compiler api immediately expands a type alias to it's definition, so let i: int64 would be equal to let i: number. I could read the type annotation, but that would break all type inference:

type int32 = number;
function returnsInt(): int32 {
return 0;
}
let i = returnsInt();

Checking the existence of a property is simple using the compiler api, while this approach would (if I'm not mistaken) require a lot more work.

Type checking is indeed important, but at the moment I can live without it.

@jbondc
Copy link
Contributor

jbondc commented Sep 2, 2015

Hmm right, some hack would be needed here:
https://github.com/Microsoft/TypeScript/blob/master/src/compiler/checker.ts#L2916

Replace
getTypeFromTypeNode() -> getAliasTypeFromTypeNode

which would 'create' new intrinsic primitive types:
https://github.com/Microsoft/TypeScript/blob/master/src/compiler/checker.ts#L103

type int32 = number; // number type #1 with intrinsicName == 'int32'
type int64 = number; // number type #2 with intrinsicName == 'int64'

Think you'd need to change === numberType in several places to (type.flags & TypeFlags.Number).

Best quick hack I can think of 😄

@jbondc
Copy link
Contributor

jbondc commented Sep 2, 2015

@ivogabe Here's the hack:
master...jbondc:number-intrinsic-hack

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 2, 2015

@jbondc Thanks! I think I can get it working with that. Downside of that is that I need to maintain a fork, what I prefer not to do.

Would the team be interested in merging such feature, or a different way to create subtypes of primitives?

@mhegazy
Copy link
Contributor

mhegazy commented Sep 2, 2015

Would the team be interested in merging such feature, or a different way to create subtypes of primitives?

I do not think so. this is a very specialized hack. I would think there is room in the future for supporting more built in integer types, e.g. int, int32, int64..etc.. that would be subtypes of number.

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 3, 2015

I would think there is room in the future for supporting more built in integer types, e.g. int, int32, int64..etc.. that would be subtypes of number.

Can you give some more details on this? Would this mean that these are hard coded or user defined? Are you referring to set types (with ranges)? I can try to write a proposal and PR if you'd like.

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 3, 2015

I've an idea that would fit well in TypeScript: it doesn't require type information for emit (thus works with isolatedModules), is predictable and doesn't have issues with overflows. It also emits code that can be optimized by JS engines. I'll work it out and create a proposal.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 3, 2015

@ivogabe we do not have plans for doing this in the near term. but would be interested to see your proposal.

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 4, 2015

I've written the proposal, see #4639

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 5, 2015

safeint would be roughly equal to int54, except that an int54 starts at -9007199254740992. Defining numbers with the amount of bits has several advantages, you can easily write a conversion function (eg x | 0 or (x | 0) & 256) and browsers can optimize code that uses these conversions. I did a quick test, all (modern) browsers optimize code that uses x | 0. The other conversions are less optimized, but especially in Safari you can get a big speed increase with them too.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 10, 2015

with #4639, i do not think this proposal is needed any longer.

@mhegazy mhegazy closed this as completed Sep 10, 2015
@mhegazy mhegazy added Declined The issue was declined as something which matches the TypeScript vision and removed In Discussion Not yet reached consensus labels Sep 10, 2015
@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 10, 2015

Agree about that.

I figured out I can use this today:

namespace std {
    enum Int8 {}
    enum Uint8 {}
    // etc

    export type int8 = number | Int8;
    export type uint8 = number | Uint8;
}

Using this I can distinguish different number types using the compiler api. Built in support for integers would be better though.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants