Strongly enforced type aliases

Topics: Language Specification
Apr 2, 2013 at 4:39 AM
Problem

When typing variables as "string"s or "number"s, we often throw out important semantic information about the value of the variable. In keeping with the type safe ideals of TypeScript, I'd like to propose for discussion the idea of "strongly enforced" type aliases. Consider the following example
interface Student {
  id:  number; // internal student number for the school
  ssn: number; // social security number
}

function doStuffForStudent(studentNumber: number) { ... }

var student: Student;

doStuffForStudent(student.id);
doStuffForStudent(student.ssn);
The problem is that both of these function calls type check, yet only one will behave correctly assuming a reasonable implementation of doStuffForStudent. This sort of problem, in my opinion, is a typing problem. (And yes, better variable names will make the code more readable and thus users will have a higher chance of passing the right value in, but that's not the point.)


Proposed Solution

We can give a much tighter type to these members and parameters, and catch more errors at compile time. Consider the proposed alternative:
type StudentId: number;
type SocialSecurityNumber: number;

interface Student {
  id:  StudentId;
  ssn: SocialSecurityNumber;
}

// yes, the formal parameter should be better named, but that's not the point
function doStuffForStudent(studentNumber: StudentId) { ... }

var student: Student;

doStuffForStudent(student.id);   // will type check
doStuffForStudent(student.ssn);  // will NOT type check

// Chances are that data like this comes from the backend, and is
// typed by virtue of an interface that's associated with the data
// payload coming from the backend. Still, sometimes there is a need
// to assign an explicit value. In such cases, explicit declaration of
// intent reads beautifully:

student.id  = <StudentId>123456;
student.ssn = <SocialSecurityNumber>987654321;
In the Current Spec

What can we do to approach this solution within the current spec? Classes and interfaces cannot extend primitive types, so they cannot be used generally, and interfaces are too permissive anyway: even if you had
interface StudentId extends number {}
you'd be able to assign 123 to var id: StudentId, and it would type check, without an explicit cast.

An interesting hack one can in theory do is to use dummy classes for compile time type checking, and rely on type erasure to have everything hum at runtime with primitive type values.
class StudentId {}

function doStuffForStudent(studentNumber: StudentId) { ... }

doStuffForStudent(student.id); // type checks
We'd probably want to have these dummy classes have special names so that users know what they are for. Maybe StudentId$$ or something.

In practice this doesn't work because
var id: StudentId = <StudentId>(123);
does not compile. I thought explicit casts are supposed to override any compiler decisions, but apparently that's not how it works. Explicitly casting <StudentId>({}) does work, for the record.

Either way, this is an interesting type system hacking exercise at best, and any real solution would need to be supported by the compiler.


Thoughts?

So, thoughts? I don't know what's involved in getting this idea attention, but I feel that the idea is cheap to implement, yet can be highly valuable in terms of additional logic errors caught at compile time.
Apr 2, 2013 at 5:00 PM
What you're proposing is basically typedef from C/C++. That's a feature with a long history and I'm sure a lot of people have strong feelings both for and against it.

I think the example you give is certainly a good use of typedef, though in my own code I would prefer the function doStuffForStudent to take in a Student object unless ssn can exist outside of Student.

The biggest problem with your proposal is if all typedefs are in the global namespace then collisions are inevitable. JavaScript has come a long way in the last decade to get everything out of the global namespace so libraries can play nicely together and it would a waste of go backwards. Does applying the typedef to the namespace it was declared in make sense? Would it be weird if School.Package.StudentId was really an alias for a primitive type?
Apr 3, 2013 at 12:28 AM
Thanks for replying.
doStuffForStudent should take a Student object
This is my failure at finding the perfect example that is small, easy to understand, illustrates the problem completely, yet does not have any holes. Indeed, taking a Student object would make sense in this example. This does not diminish the problem that I'm trying to address. Perhaps a better example would be the following:
interface Student {
  id:  number; // internal student number for the school
  ssn: number; // social security number
}

interface UpdateNameRequestPayload {
  id:   number;
  name: string;
}

var student: Student;

var payload: UpdateNameRequestPayload = {
  id:   student.ssn,   // incorrect data, but type checks fine
  name: "New Name"
};

someLibrary.ajaxPost("/students", payload);
versus
type StudentId: number;
type SocialSecurityNumber: number;

interface Student {
  id:  StudentId;
  ssn: SocialSecurityNumber;
}

interface UpdateNameRequestPayload {
  id:   StudentId;
  name: string;
}

var student: Student;

var payload: UpdateNameRequestPayload = {
  id:   student.ssn,   // type check fails at compile time
  name: "New Name"
};

someLibrary.ajaxPost("/students", payload);
if all typedefs are in the global namespace
Nothing necessitates this, just like nothing necessitates that classes are all declared in a global namespace.
module myModule {
  export type StudentId: number;

  export interface Student {
    ...
  }
}
I don't see it as any different from what we have with classes and interfaces, and other modules.
Does applying the typedef to the namespace it was declared in make sense?
It makes sense to me. If it does not make sense to you, could you please explain why?
Would it be weird if School.Package.StudentId was really an alias for a primitive type?
I do not think it would be weird. If you do feel that way, can you please define "weird" and explain what you mean concretely? Sorry if I'm missing something obvious that you're hinting at here.
Apr 3, 2013 at 4:06 PM
We have the following use case:
interface Guid extends String {
}

var guidBad: Guid = "oops!"; // Should fail, but currently compiles
var guidGood: Guid = <Guid> "516FEF91-788C-44G2-B5C8-89A36ECB777B"; // Should work, and currently works
}
I believe this is all that is required? Basically, we want the developer to say "trust me, that is a GUID".

We'd like to see a solution to this but not at the expense of introducing typedef which is open to abuse, and can make reading code very difficult on account of custom types. Furthermore, the use case is very small and IMO does not warrant a new keyword.

Perhaps if there is a way of decorating the interface so that an explicit cast is required?
[Explicit]
interface Guid extends String {
}
or
explicit interface Guid extends String {
}
Yes, this is introducing new syntax, but both the attribute and the explicit keyword would have applications in other contexts.

Noel
Coordinator
Apr 3, 2013 at 4:58 PM
@nabog - enforcing this kind of stricter compatibility rules and requiring casts is something that generally falls outside of a structural type system and is more inline with the types of rules you see in nominal type systems. When you say <Guid>, it's not actually a cast from one type name to another. It's an assertion that the shape of the thing on the right hand side is assignment compatible with the structure of the type given (Guid in your example).

There are some cases where you can enforce some strictness, for example two classes with the same public members aren't structural compatible if either class has private members.
Apr 3, 2013 at 5:13 PM
can you please define "weird" and explain what you mean
I can't think of any specific reasons it would be bad but I've never seen it before so I think it's worth drawing attention to. I just want to make sure everything is thought through, not be passive aggressive.

I'm also wondering where casting would be required and where it wouldn't.
type StudentId: number;
StudentId idOne = 5; // error requires cast
StudentId idTwo = <StudentId> 5; // ok
StudentId idThree = <StudentId> 6; // ok

idTwo++; // is this ok?
idTwo + idThree; // I think this makes sense
var w: number = idTwo; // is this ok?
var x: number = idTwo + 3; // is this ok?
var y: StudentId = idTwo + 3; // is this ok?

type SuperStudentId: StudentId; // what are the implications of this?
I refreshed my memory in C a little bit and noticed that typedef aliases the type but doesn't actually require any casting. So following C's lead definitely doesn't get you what you want.
Apr 10, 2013 at 9:10 AM
@Grajkowski
I just want to make sure everything is thought through, not be passive aggressive.
You're right, I was focused on what benefits I want to reap from the proposed extension of the type system, and paid barely any attention to the consequences, potential inconsistencies, etc.

@jonturner

Preemptive apologies for potentially dumb questions, I'm a layprogrammer and know little about type theory.

If TypeScript's type system is structural, then is it impossible to differentiate between an instance of number and and instance of some sort of StudentId inherits-but-is-distinct-from number, because structurally the two types are assignment compatible? So even if we assumed that what I'm proposing is a reasonable idea, it falls outside of the realm of structural type systems, and thus cannot be [easily] implemented in tsc? Is it basically the same problem as this:
interface Person {
    name: string;
}

interface Pet {
    name: string;
}

var jack: Person = { name: "Jack Sparrow" };
var pet: Pet = jack;  // type checks just fine
where the last line makes "no sense" if you're used to Java-style interface definition and inheritance, but type checks fine because Pet and Person are structurally equivalent?

Am I understanding it correctly?
Coordinator
Apr 10, 2013 at 4:23 PM
@anchann - That's exactly right. Structural type systems don't care what the name of the type is, they simply look at the components of the type.

Using just interfaces, you can't tell a difference between Person and Pet. This is one of the drawbacks of structural systems, but they also give you the flexibility to handle situations where you only care about what properties an object has without knowing specifically what its original class was. That's the case with TypeScript, since you can create objects out of thin air in JavaScript without any named class that they're constructed from.
Apr 22, 2013 at 5:14 AM
Just to rant a little, real functional languages have had algebraic types as their backbone for decades; structural typing (in the form of type classes) rather takes second place in most functional programs.

I really wish the OO world would take a good look at including algebraic types; I can't think of any good reason not to have them, while the benefits would be great.