microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.06k stars 12.49k forks source link

Mention structural typing in Quick Start #11954

Closed Arnoku closed 4 years ago

Arnoku commented 8 years ago

TypeScript Version: 2.0.3 / nightly (2.1.0-dev.201xxxxx)

Code

// A self-contained demonstration of the problem follows... In your Quick start documentation (https://www.typescriptlang.org/docs/tutorial.html), could you please fix or make more clear on the bottom of the page in the Classes section that Interface and Class in the example ARE NOT RELATED, and that the nature of TypeScript is that it can be even an empty object without instantiating any class at all like so:

greeter({ firstname: "John", lastname: "White" });

Since coming from a partially OOP background, it confused me a lot how come we instantiate a Student class object, but the function expets a Person interface object, and there is no connection between them except for same property names. Cheers

Expected behavior:

Actual behavior:

jtulk commented 8 years ago

I'm stuck on this Quick Start example as well. The progression towards the final bit of code:

class Student {
    fullName: string;
    constructor(public firstName, public middleInitial, public lastName) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
}

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person : Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

var user = new Student("Jane", "M.", "User");

document.body.innerHTML = greeter(user);

makes it seem like the Person interface is actually going to throw errors if var user = new Student("Jane", "M.", "User"); is modified to something like var user = new Student(1, "M.", 2);, but in reality the code compiles and runs just fine on both the command line and in Visual Studio Code.

So the obvious questions would be:

  1. Why isn't TypeScript throwing errors? (Why isn't the interface connected when greeter explicitly uses it?), and
  2. How should this be rewritten so that greeter(), Student, and Person are connected?
RyanCavanaugh commented 8 years ago

I don't understand what's being requested.

make more clear on the bottom of the page in the Classes section ... that it can be even an empty object without instantiating any class at all like so: greeter({ firstname: "John", lastname: "White" });

This is literally an example on the page already, with the minor variation of storing that object in an unannotated var first

var user = { firstName: "Jane", lastName: "User" };

document.body.innerHTML = greeter(user);
aluanhaddad commented 8 years ago

I think the request is just to assert the structural nature of the type system earlier in the introductory material on classes. I'm not sure what the OP has in mind but this seems to trip up people who come in with preconceived notions of what a class or an interface is from nominally typed languages.

aluanhaddad commented 8 years ago

@jtulk the issue with this class, which is by no means broken,

class Student {
    fullName: string;
    constructor(public firstName, public middleInitial, public lastName) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
}

is that arguments (here parameter properties) of the constructor are not given an explicit type and there is no inference context. You might want to write this:

class Student {
    fullName: string;
    constructor(
        public firstName: string,
        public middleInitial: string,
        public lastName: string
    ) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
}
Arnoku commented 8 years ago

@RyanCavanaugh

It's not intuitive. I expected TypeScript to support and filter by instances of an object. Follow along will you please:

We have 3 declarations: class Student, interface Person and function greeter which expects it's argument to be (at least in my mind coming from PHP, and what I've seen in other languages) an instance of a Person. So in this case, I'm expecting that the argument passed in the function is an instance of that Interface/Class, which it is not in the example.

Then user is a new object instance of **class Student***.

In the last line we are giving the function greeter a user parameter, which in fact is an instance of a Student class. But it's not an instance of Person class. Person and Student are not at all related.

It could be as:
class Student extends Person {

but it's not, and it confused me quite a lot how come that script works, and how are Student at all related to Person if it's not explicitly in the code above. It's confusing!

Later on I've read that there's no such thing as looking up if the object is an instance at all, and you can do:

document.body.innerHTML = greeter({ firstname: "John", lastname: "White" });

Which is an object that is not instantiated from any class or Interface.

That is why I highly suggest you mention everything I said in a way, because it might cause confusion for other people coming from OOP environments

aluanhaddad commented 8 years ago

@Arnoku It is only confusing if one has preconceptions they are not aware of. I agree it can be confusing to newcomers but I think you have no logical basis for saying:

We have 3 declarations: class Student, interface Person and function greeter which expects it's argument to be (at least in my mind coming from PHP, and what I've seen in other languages) an instance of a Person. So in this case, I'm expecting that the argument passed in the function is an instance of that Interface/Class, which it is not in the example.

Arnoku commented 8 years ago

@aluanhaddad it expects Person, it receives Student. Student and Person are not related, am I the only one who thinks it's very confusing? I don't know how else can I explain this
And it took me personally some time to research how those two could be related. I'm trying to help by saying you should fix the manual and add more clarity to it, one sentence and example, and you're saying that I have a preconceptions

Just maybe add:
While the function expects an argument to be a Person interface, you may pass any object that contains properties, defined in the interface.

greeter({ firstname: "John", lastname: "White" });

Or at least put the line with the code in the Quick start so people could at least see it and maybe that will make then understand it more clearer

aluanhaddad commented 8 years ago

@Arnoku Firstly, I am not a member of the TypeScript team and cannot "fix" the manual. Secondly, I upvoted your OP, I agree this can be confusing.

The point I was trying to make was that people should not assume a language behaves a certain way because other languages behave that way.

Anyway, the types are related, because the type system is structural and they describe compatible shapes. While this might not be intuitive for someone coming from Java, it might well be intuitive for someone coming from Python or say JavaScript.

RyanCavanaugh commented 8 years ago

@Arnoku this is your one and only warning to follow the Code of Conduct: https://opensource.microsoft.com/codeofconduct/

(edit: original comment I was referring to contained a lot of profanity; comment as it stands now is obviously fine)

Arnoku commented 8 years ago

@RyanCavanaugh sorry, I've edited the answer, heat of the moment thing
It just truly bugged me, hope you guys understand why it bugged me and people in the future will understand it better than I did

Cheers and thank you for taking your time to actually read it and care about this :)

jtulk commented 8 years ago

@aluanhaddad I'm not meaning to imply that the class is broken- what I'm trying to imply is that the flow of the tutorial is confusing.

The first thing you do in the tutorial is use type annotations to throw errors if the parameters do not match the contract:

function greeter(person: string) {
    return "Hello, " + person;
}

Then you create an interface to make sure the shape of the object passed to greeter matches:

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person: Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

var user = { firstName: "Jane", lastName: "User" };

document.body.innerHTML = greeter(user);

Then finally a class is added alongside, but the greeter function is still left associated with the interface.

class Student {
    fullName: string;
    constructor(public firstName, public middleInitial, public lastName) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
}

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person : Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

var user = new Student("Jane", "M.", "User");

document.body.innerHTML = greeter(user);

Am I completely wrong in thinking that the structure of the tutorial (and the syntax) leads me to believe that greeter(person: Person) will try to apply the interface to the argument it's given? Even if that argument is an instance of class Student? The way the tutorial is the class doesn't require any specific data types, but the interface (which is still left in the greeter function definition) still does. So why doesn't calling greeter(Student) throw errors when Student doesn't match the interface (such as by using var user = new Student(1, 2, 3);)?

RyanCavanaugh commented 8 years ago

Student does match the interface. But it uses parameter properties, which are easy to miss. Overall we should just rewrite the entire Quick Start; it's too class-focused and doesn't directly explain many core concepts in a good way.

jtulk commented 8 years ago

@RyanCavanaugh Sorry, in the example you're correct. What I mean is that if you change var user = new Student('Jane', 'M.', 'User'); to var user = new Student(1, 2, 3); the code will run w/out any TypeScript errors. So it seems like the interface restrictions get completely dropped somehow.

(Edited my comment above for future clarity)

RyanCavanaugh commented 8 years ago

That's because the firstName / etc fields are of type any because they don't have type annotations

jtulk commented 8 years ago

@RyanCavanaugh - right, but the interface stipulated in the greeter function is just completely ignored then? If so, WHY?

function greeter(person : Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

Why does the fact that we made a separate class and passed it into greeter mean the interface explicitly set in greeter() no longer does any shape-checking? Do you really not understand at all what I'm asking here? I feel like I'm taking crazy pills.

Arnoku commented 8 years ago

I understood what @jtulk is trying to say and now that confuses me as well.
You're asking why the interface has a type set to string, yet when you pass arguments that are int they still get compiled? If this is true I don't get that point as well

Maybe that means that it just checks for the properties being present?

jtulk commented 8 years ago

@Arnoku Exactly. It seems to me that the whole point of defining an interface is to enforce a contract for the data that gets supplied into the function. Is this functionality supposed to break as soon as you stop passing in an object literal?

RyanCavanaugh commented 8 years ago

At the risk of rewriting the entire Handbook or spec in this thread...

The type Student has the shape

{ fullName: string; firstName: any middleInitial: any; lastName: any }

Those three fields have the type any because the constructor parameter properties don't have type annotations:

    constructor(public firstName, public middleInitial, public lastName) {

The implements clause says "Make sure this class is an acceptable substitute for this interface". The Student type fulfills the requirement because its fields, which are of type any, are acceptable substitutes for the string fields defined in the interface.

Because the parameters are of type any, you can pass in any value.

We could "fix" this by adding type annotations:

    constructor(public firstName: string, public middleInitial: string, public lastName: string) {

at which point you could no longer new the class using numbers.

So in closing, if you don't want surprising-ish things to happen, avoid fields of type any.

jtulk commented 8 years ago

@RyanCavanaugh I'm not sure how you keep missing the question here. No one is asking about the any types on Student. We're asking specifically about the

"Make sure this class is an acceptable substitute for this interface"

part you mentioned. If the interface specified requires firstName and lastName to be of type string, and we pass class Student of which firstName and lastName are numbers (which is fine according to the Student shape w/ type any), HOW DOES THIS NOT FAIL THE INTERFACE SPECIFICATION?

Shouldn't var student = new Student(1, 2, 3); fail because firstName: 1 and lastName: 3 fail the interface specification which requires them to be of type string?

roganov commented 8 years ago

No, firstName and lastName are not numbers, but any, and any can be substituted for string (in particular).

RyanCavanaugh commented 8 years ago

Here's an analogy.

Your accountant says that she needs your forms on 8x11" paper.

Your courier says he can transfer paper of any size to any person.

You give your courier A4 size paper and an address, and he delivers it to your accountant, who is surprised because your forms are the wrong size despite her explicit instructions.

WHY DIDN'T YOUR COURIER WARN YOU ABOUT THE WRONG SIZED PAPER?

Well, your courier said he can deliver any size paper. Your accountant had a specific requirement, but using the non-caring intermediate caused her constraint to be violated. What you wanted was a typed courier that would only accept 8x11" paper. Then your courier would have rejected your A4 forms.

jtulk commented 8 years ago

@RyanCavanaugh Thanks - I think this helps. In your analogy the accountant is surprised because the paper is the wrong size. Obviously the courier didn't know, but the accountant 'throws an error' right? In this code, run either through the command line tsc compiler, or the Visual Studio Code build, the accountant is never surprised. That's what I'm driving at. I'm not blaming the courier, I'm saying, "Why does the accountant stop caring about her initial requirement?"

The interface:accountant should complain about type:paperSize in the following snippet, right?

class Student {
    fullName: string;
    constructor(public firstName, public middleInitial, public lastName) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
}

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person : Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

var user = new Student(1, 2, 3);

document.body.innerHTML = greeter(user);

// browser output
=> Hello, 1 3
aluanhaddad commented 8 years ago

The any type is compatible with all types. It is a static formalization of JavaScript's dynamic behavior.

jtulk commented 8 years ago

Good Lord. If you guys are trolling me you're doing brilliantly.

AGAIN, I UNDERSTAND THAT THE DATA TYPES FOR THE STUDENT CLASS ARE ANY. See: https://github.com/Microsoft/TypeScript/issues/11954#issuecomment-258557806

What I am asking is WHY THE INTERFACE CONTRACT IS NOT ENFORCED FOR THE GREETER FUNCTION. It says, "give me an object of shape {firstName: string, lastName: string}, but when supplied with a Student-Class object where {firstName: number, lastName: number} it ignores the interface contract and compiles without errors. WHY?????

@RyanCavanaugh gives the analogy of an accountant (the interface) who wants paper a certain size, but the courier (the Class) agrees to carry paper of any size. But what is happening in this tutorial is that the courier's preference (paper size: any) clobbers the accountant (paper size 8" x 11").

Or let's put it this way. If I buy a car that only accepts Premium gas, the fact that a gas pump can carry fuels of any octane level does nothing to change this. If I put in regular unleaded just because the pump is allowed to pump it, my car SHOULD knock, right? It shouldn't run flawlessly just because the pump has a less rigorous acceptance criteria.

roganov commented 8 years ago

supplied with a Student-Class object where {firstName: number, lastName: number}

As have been said already, this is not true. The type of Student class is {firstName: any, lastName: any, middleName: any, fullName: string}, it just so happens that you instantiated Student with numbers. To illustrate:

var x = 1;
x; // number
var s = new Student(x, 2, 3);
s.firstName; // any
Arnoku commented 8 years ago

@roganov That is not what he is talking about I think

interface Person {
    firstName: string;
    lastName: string;
}

The interface has strict types of string

function greeter(person : Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

var user = new Student(1, 2, 3);

document.body.innerHTML = greeter(user);

I want you to pay close attention to this piece of code. We send the allegedly instance of a Student object, it can have any type of firstName, lastName like you mentioned

Now pay attention to greeter function. It expects an object that would match the declaration of the interface Person. The Person interface expects 2 parameters to be string. That's the whole point!

We instantiate the object as Student, no strict parameters, but greeter function expects those 2 parameters to be strictly string. That is what @jtulk is trying to say I think, and I haven't done this myself, but the logic holds here for me, and if you can pass integer arguments to the greeter function, then that's quite a weird behaviour if you ask me.

roganov commented 8 years ago

greeter function expects those 2 parameters to be strictly string

This is not quite correct, greeter function expects an argument that implements the Person interface, and Student does implement the Person interface because any is substitutable for string.

jtulk commented 8 years ago

@Arnoku Exactly.

It seems like the code is analogous to something like:

const allowedData = 'string'

const greeter = data => {
  if (typeof data !== allowedData) {
    throw `Datatype must be a ${allowedData}`
  } else { 
    console.log(data)
  }
}

const dataInstance1 = 'Hello'
const dataInstance2 = 3
const dataInstance3 = []

greeter(dataInstance1) // 'Hello'
greeter(dataInstance2) // error
greeter(dataInstance3) // error

But @roganov is saying that

This is not quite correct, greeter function expects an argument that implements the Person interface, and Student does implement the Person interface because any is substitutable for string.

Can you explain this in more detail? This seems really, really counter-intuitive based on the semantics of the code itself. Or more clearly, it seems bizarre that greeter would know anything the data types allowed in Student class. I don't know how the internals of TypeScript works (obviously), but it seems like there would be two steps based on order of execution:

  1. var user = new Student(1, 2, 3); It seems like here TS would check the arguments against the allowed data types (any) and successfully create the object { fullName: 1 2 3}, then
  2. greeter(user) This seems like it would evaluate the user object against the contract enforced by the Person interface. Besides failing on type - at this point the object seems like it wouldn't even have firstName or lastName properties

But if I'm understanding you, you're saying that TS goes back up the chain somehow? How? Why? Where is this documented?

jtulk commented 8 years ago

From the docs:

interface LabelledValue {
    label: string;
}

function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

The interface LabelledValue is a name we can now use to describe the requirement in the previous example. It still represents having a single property called label that is of type string. Notice we didn’t have to explicitly say that the object we pass to printLabel implements this interface like we might have to in other languages. Here, it’s only the shape that matters. If the object we pass to the function meets the requirements listed, then it’s allowed.

It’s worth pointing out that the type-checker does not require that these properties come in any sort of order, only that the properties the interface requires are present and have the required type.

The docs say "only the shape matters" and in the example above, the shape is pretty much wholly ignored.

roganov commented 8 years ago

public and private parameter modifiers are shortcuts that automatically declare and set class properties. So that

class A {
  constructor(public x) {}
}

is a shortcut for

class A {
  public x: any;
  constructor(x) { this.x = x };
} 

This is actually explained in the tutorial:

Also of note, the use of public on arguments to the constructor is a shorthand that allows us to automatically create properties with that name.

jtulk commented 8 years ago

@roganov Oh nice- I missed that part. Thanks. Still doesn't explain why the Person interface isn't enforced though.

roganov commented 8 years ago

I'm not sure what you mean by "Person interface isn't enforced", but as have already been said, instances of Student are valid arguments to greeter function because { firstName: any; lastName: any; middleInitial: any; fullName: string } (shape of Student) is substitutable for { firstName: string; lastName: string} (shape of Person).

jtulk commented 8 years ago

@roganov Okay - I think I'm getting what you're saying, let me try to rephrase to make sure.

This:

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person : Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

creates a contract where greeter() expects an argument that matches the shape of Person. However, if you supply an argument that matches the shape of Person (i.e. has a firstName and lastName property) but has its own datatype contract for those properties (here the data being of type any) then THAT contract is enforced on the data typing?

This still seems really counter-intuitive to me, but if that rephrasing is correct I can sort of grok what's going on here.

roganov commented 8 years ago

has its own datatype contract for those properties (here the data being of type any) then THAT contract is enforced on the data typing

I'm not sure what is "THAT contract", but there is just one "contract": that argument to greeter is of type Person, which means that the argument needs to have firstName and lastName properties and their type needs to be substitutable for string. The key is to understand that any is substitutable for any type (to string in particular), which makes Student substitutable for Person.

leonroy commented 7 years ago

Useful discussion - after completing the Quickstart I literally had the same question. For future TS/JS newbies (like myself) it's helpful to look at the compiled output of greeter.js to see that the function greeter simply expects an Object literal containing the keys: firstName and lastName. The compiled output for Student satisfies this.

This discussion actually makes the Quickstart examples a pretty good lesson in JavaScript's weak and dynamically typed nature for those coming from strongly typed and especially statically typed languages like Java.

RyanCavanaugh commented 4 years ago

We've rewritten the handbook and, frankly, this thread is a mess.