tc39 / proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
https://tc39.es/proposal-type-annotations/
4.13k stars 44 forks source link

The proposal might be trying to do too much #211

Open temeddix opened 4 months ago

temeddix commented 4 months ago

Hi, please let me share my opinions about this proposal, because this proposal seemed to get more and more overwhelming. I know I'm not in any position to direct things, but after many months of community debate, I wanted to suggest a new point of view.

I am not sure if the proposal is heading the right direction. To me, it looks like the whole discussion is influenced too much by the way TypeScript does types, including things that are not really necessary.

To do and not to do

The philosophies of JavaScript and TypeScript about handling types are different. JavaScript uses simple prototype-based class system, while TypeScript uses interface, type, and class.

The reason for 3 different syntaxes for declaring types in TypeScript is known to be a historical path of TypeScript, and there have been a lot of controversies around TypeScript community for many years(about when to use what). I would argue that this system is not ideal, and it has a lot of technical debt.

What if we stick to the current prototype-based class system in JavaScript, only with the new 'type annotation'? This would be similar to Python's 'erasable' type hint system. This way, the script size doesn't get too bigger than now, but type safety is achieved.

class Box<T> {
    item: T;
    size: number;

    constructor(item: T, size: number) {
        this.item = item;
        this.size = size;
    }
}

const stringBox = new Box<string>("Hello", 5);
console.log(stringBox.item); // Output: Hello
console.log(stringBox.size); // Output: 5

const boolBox = new Box<boolean>(true, 10);
console.log(boolBox.item); // Output: true
console.log(boolBox.size); // Output: 10
function combine(a: string?, b: number): string {
    if (string == null) {
        return "";
    }
    return a + b.toString();
}

const combinedString = combine("Hello", 42);
console.log(combinedString); // Output: Hello42

Dealing with JSON

Then we come up with a question.

"What about type safety for JSON configuration files and JSON APIs?"

Yes, it is a common practice to use interface and type statements in TypeScript to structure the JSON schema nowadays. However, it can definitly be done with the existing class system of JavaScript without the complexity of TypeScript.

Take a look at this Rust example:

#[derive(Serialize, Deserialize)]
struct Address {
    street: String,
    city: String,
}

#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
    address: Address, // Nested struct
}

let person = Person {
    name: "Alice".to_string(),
    age: 30,
    address: Address {
        street: "123 Main St".to_string(),
        city: "Anytown".to_string(),
    },
};

let serialized = serde_json::to_string(&person).expect("Failed to serialize");
let deserialized: Person = serde_json::from_str(&serialized).expect("Failed to deserialize");

Rust elegantly achieves nested JSON schema with type-safety like above. In fact, there's a JavaScript library dedicated to achieve the same thing:

This library contends that we should use classes for type-safe JSON schemas.

Its ES6 and Typescript era. Nowadays you are working with classes and constructor objects more than ever. Class-transformer allows you to transform plain object to some instance of class and versa. Also it allows to serialize / deserialize object based on criteria. This tool is super useful on both frontend and backend.

Currently, class-transformer library requires a bit of boilerplate code like @Type(() => Photo), and I think this proposal can include a new method like Object.getTypeAnnoationOf(MyClass, 'fieldName') to support typed serialization/deserialization of nested JSON. Maybe we can provide an optional type argument for JSON functions, like this:

let jsonString = `
{
  "id": 1,
  "name": "foo",
  "photos": [
    {
      "id": 9,
      "filename": "cool_whale.jpg",
      "depth": 1245
    },
    {
      "id": 10,
      "filename": "hot_bird.jpg",
      "depth": 6123
    }
  ],
  "pages": {
    "table": 6,
    "contents": 72
  }
}
`;

@autoConstructor
class Photo {
  id: number;
  filename: string;
  depth: number;
}

@autoConstructor
class Pages {
  table: number;
  contents: number;
}

@autoConstructor
class Album {
  id: number;
  name: string;
  photos: Array<Photo>;
  pages: Pages;
}

let album: Album = JSON.parse(jsonString, Album)  // <-- Optional type argument
console.log(album);

Array type representation

The TypeScript team somehow decided that an array of some type is represented as MyClass[], not Array<MyClass> (though both work). MyClass[] syntax is used in low-level languages such as C, C++, and Rust, where an array is actually a continous memory space. However, this does not really make sense for high-level languages like JavaScript. Perhaps Array<T> should be explicitly required as a language spec.

azder commented 4 months ago

I agree on it doing too much, but that doesn't make what you propose not also too much. But hey, I proposed no syntax change to JS (https://github.com/tc39/proposal-type-annotations/issues/176), so everything will look too much compared to it.

Anyway, if there is a syntax change, why should it be in the form of TS/C#/Java? Just because that's what people are used to? What you are used to?

The way I proposed it - using comments, it's extensible with TS syntax (as long as you don't polute the actual JS code, but put your type and interface in a comment), but it also doesn't hamper some other kinds of extensions, other ways of type system notation.

ReinsBrain commented 3 months ago

removing interfaces and common typed-array syntax would be disappointing for me