Skip to content

Derived types #2477

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
mindplay-dk opened this issue Mar 24, 2015 · 4 comments
Closed

Derived types #2477

mindplay-dk opened this issue Mar 24, 2015 · 4 comments
Labels
Duplicate An existing issue was already created

Comments

@mindplay-dk
Copy link

I don't know if "derived" is the correct term, but, currently, I can do something like this:

type UserID = number;

type Timestamp = number;

var id = <UserID> 123;

var time = <Timestamp> 12345678;

var nonsense = id + time;

The problem is obvious - different types of IDs (all numbers) and even completely unrelated datatypes (such as timestamps) are all interchangeable; there is no type-safety for different types of data that all happen to be integers.

I would like to be able to do something along the lines of this:

type UserID: number;

type Timestamp: number;

var id = <UserID> 123;

var time = <Timestamp> 12345678;

var nonsense = id + time; // ERROR

The difference here, is that UserID and Timestamp are actually distinct types derived from a base type (in this case a primitive), not just aliases of number.

The derived type is similar to the type it was derived from - it has all of the same members.

It can be safely cast back to the type it was derived from, for example:

type UserID: number;

var user_id = <UserID> 123;

var id: number = user_id; // VALID

You cannot however automatically cast back up:

var id = 123;

var user_id: UserID = id; // ERROR

But you can of course do so with an assertion:

var id = 123;

var user_id = <UserID> id; // VALID

In other words, UserID is always a number, but number is not necessarily a UserID.

Another example might be a point type:

class Point {
    x: number;
    y: number;
}

type Centroid: Point;

var center: Centroid = { x: 1.234, y: 2.345 };

var p: Point = center; // VALID

var oops: Centroid = p; // INVALID

Again, a Centroid is always a Point, but a Point is not a Centroid unless you make that assertion.

In short, this feature enables static type-checking of types that are technically identical - there is only one implementation, but there is more than one meaning.

Other examples might be "email", "phone number", "credit card", "IP address" and "hex color code", which are all strings, each with a particular meaning, all of which can be treated as strings, but none of which are interchangeable in any meaningful way.

@CyrusNajmabadi
Copy link
Contributor

Here's a simple way to get your Point/Centroid example working:

interface Point {
    x: number;
    y: number;
}

interface Centroid extends Point {
    _centroidBrand: any;
}

Now you can assign any centroid to a point, but you will get an error if you assign a point to a centroid.

For things like PhoneNumber, etc, i would do something like:

interface PhoneNumber {
    _phoneNumberBrand: any;
}

function createPhoneNumber(s: string) {
    return <PhoneNumber><any>s;
}

function unsafeGetPhoneNumberContents(p: PhoneNumber): string {
    return <string><any>p;
}

Now you could pass around a phone number in your system and not ever have it be convertible to anything else implicitly. When you did want to actually get at the contents, you'd explicitly state you wanted the string form for it. You could then use that string form only for as long as necessary, and never leak that internal representation out.

(Of course, you could just do this by wrapping the underlying string in a PhoneNumber class as well. but this approach allows you to avoid that wrapper, and allows you to just pass around the string as is, with no actual runtime overhead).

@mindplay-dk
Copy link
Author

That's interesting, but doesn't really work.

interface UserID extends Number {
    _brand: any;
}

var id = 123;

var user_id = <UserID> id; // ERROR

Or at least not without a double assertion:

var user_id = <UserID> <any> id;

Using an empty interface actually probably comes closer to what I want:

interface UserID extends Number {}

var id = 123;

var user_id: UserID = id; // bad: this shouldn't work

var nonsense = id + user_id; // good: doesn't work (and shouldn't)

Is "tainted" the correct terminology here? If so, maybe a keyword could be used to annotate interfaces, instructing the compiler to treat it as if it were different, when doing an implicit cast, even though it isn't:

tainted interface UserID extends Number {}

Or maybe:

tainted type UserID = number;

Anyhow, the syntax is not terribly important to me.

@mhegazy mhegazy added the Suggestion An idea for TypeScript label Mar 24, 2015
@danquirk
Copy link
Member

Check out #364, I think that covers basically everything you want.

@mhegazy mhegazy added the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Dec 9, 2015
@RyanCavanaugh RyanCavanaugh added Duplicate An existing issue was already created and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript labels Jul 17, 2020
@RyanCavanaugh
Copy link
Member

#202

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants