Closed Arnoku closed 4 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:
greeter
explicitly uses it?), and greeter()
, Student
, and Person
are connected?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);
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.
@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;
}
}
@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
@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.
@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
@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.
@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)
@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 :)
@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);
)?
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.
@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)
That's because the firstName
/ etc fields are of type any
because they don't have type annotations
@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.
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?
@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?
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 number
s.
So in closing, if you don't want surprising-ish things to happen, avoid fields of type any
.
@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
?
No, firstName
and lastName
are not numbers, but any
, and any
can be substituted for string (in particular).
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.
@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
The any
type is compatible with all types. It is a static formalization of JavaScript's dynamic behavior.
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.
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
@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.
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
.
@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:
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}
, thengreeter(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 firstName
or lastName
propertiesBut if I'm understanding you, you're saying that TS goes back up the chain somehow? How? Why? Where is this documented?
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.
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.
@roganov Oh nice- I missed that part. Thanks. Still doesn't explain why the Person
interface isn't enforced though.
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
).
@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.
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
.
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.
We've rewritten the handbook and, frankly, this thread is a mess.
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: