Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
Loezi
Dec 18, 2012

Never buy the cheap stuff

Falcon2001 posted:

honestly I do kind of wish that Python just had a typing-enforced version

Kinda in the same boat, but then again there's some quirks of the type hinting system that have made me just throw my hands in the air before. Apparently my biggest gripe was recently fixed in mypy, but is demonstrative: https://github.com/python/mypy/issues/5876

Adbot
ADBOT LOVES YOU

champagne posting
Apr 5, 2006

YOU ARE A BRAIN
IN A BUNKER

LOOK I AM A TURTLE posted:

Unless I'm misunderstanding you, this one doesn't seem to have been applicable for a while: https://www.typescriptlang.org/play...AE1AG50uYCGkiAA. It works at least as far back as TS 3.3, which is over three years old. I think the type unification was improved somewhere around that time.

Oh no your example is 100% fine. but here comes the footgun:

In your example you have:

code:
type Glass = BaseGlass | WineGlass | BeerGlass;
Which is fine and cool, the thing that breaks but looks harmless is:

code:
type Glass = WineGlass | BeerGlass;
the error message is improved immensely since I last saw it. Now it points to the error of incompatible booleans.

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer

champagne posting posted:

Oh no your example is 100% fine. but here comes the footgun:

In your example you have:

code:
type Glass = BaseGlass | WineGlass | BeerGlass;
Which is fine and cool, the thing that breaks but looks harmless is:

code:
type Glass = WineGlass | BeerGlass;
the error message is improved immensely since I last saw it. Now it points to the error of incompatible booleans.

Ah. In my defense, I had that in my example because you had it in your example. :)

champagne posting
Apr 5, 2006

YOU ARE A BRAIN
IN A BUNKER

LOOK I AM A TURTLE posted:

Ah. In my defense, I had that in my example because you had it in your example. :)

It was a while ago I hope I misremembered. It's a big fat gun for our feet nonetheless

Volte
Oct 4, 2004

woosh woosh
That doesn't seem like a footgun at all. In fact it's the opposite of a footgun. A footgun would be if it allowed you to do something unsafe without realizing it. This is disallowing you from doing something (ostensibly) safe while explicitly telling you so. And from a type theory point of view, it makes sense. The union type is either an object that is statically known to be beer, or statically known to be wine. You can't use values to discriminate between types at runtime without a type system that allows for dependent typing, which typescript (and most other languages) don't have. If you actually want runtime polymorphism in Typescript, then you could use a class:

Playground Link

edit: and also if you're drat sure it's safe, then of course you could just cast manually. With great power comes great responsibility and all that

Volte fucked around with this message at 14:02 on Sep 26, 2022

ultrafilter
Aug 23, 2007

It's okay if you have any questions.


Carbon dioxide posted:

If TypeScript isn't strongly typed enough for you, instead you can use tooling that compiles a strongly-typed language directly into Javascript. Scala.JS comes to mind, although I've never had the chance to work with it myself so I'm not sure how well it holds up.

There's also Rescript, an OCaml to JS transpiler.

Red Mike
Jul 11, 2011
Is the complaint here that they're expecting Typescript to duck-type (which it can do to a limited degree with interfaces specifically) but then are surprised when it also tries to enforce strong typing at runtime despite the duck typing? Interfaces define the shape of a class (not the data or logic), and you're specifically defining the two child interfaces to have the shape that includes a field with a property that has only one possible value and isn't optional.

If you try to union the two interfaces (with conflicting shapes), the result is a broken shape because it needs to simultaneously contain the same field with two different values. Union of two interfaces does not lead to each property being a union of the equivalent properties from each interface; that would be insane and counter-intuitive. It would have no ability to figure out what the correct types are half the time. You have the ability to do this yourself though, even though it defeats the point:

code:
interface BaseGlass: {beverage: string, full: boolean};

interface BeerOrWineGlass extends BaseGlass: {beer: true | false};

interface WineGlass extends BeerOrWineGlass: {beer: false};

interface BeerGlass extends BeerOrWineGlass: {beer: true};

type Glass = BeerOrWineGlass;

const b1: boolean = Math.random() > 0.5;

let thirst: BeerOrWineGlass;
if (Math.random() > 0.5)
{
  const test : WineGlass = {
    beverage: 'test',
    full: b1,
    beer: false
  };
  thirst = test;
}
else
{
  const test : BeerGlass = {
    beverage: 'test',
    full: b1,
    beer: true
  };
  thirst = test;
}

console.log(thirst);
This is literally what you're trying to do, which is to have the property "beer" be a union of true and false in the base type that you can use to store the object in. But instead of making the property "beer" a union, you're trying to make a union of two object types that both define a property with different types.

Basically, a union is not the same thing as referencing sub-classes by their base class; it is defining a new "type" that is the union of the classes (so it can hold either of the two). Specifically from the docs: "TypeScript will only allow an operation if it is valid for every member of the union."

That said, the fact that TS allows duck typing like this is an unavoidable horror and I hate it. It's not a foot-gun, just a big siren that tends to go off whenever you press the wrong button until you figure out that the similarly coloured button next to it is the right button to press in fact. Over subsequent versions, the siren now even tells you what the problem is and how to avoid it, as well as making it pretty much painless to switch from one to the other (unless the problem is in a module/library).

e: corrected the example which just also highlights why it's a bad idea. You can't avoid defining beer: true or beer: false because an interface is not the same thing as a class.

Red Mike fucked around with this message at 14:49 on Sep 26, 2022

NihilCredo
Jun 6, 2011

iram omni possibili modo preme:
plus una illa te diffamabit, quam multæ virtutes commendabunt

I think this is a decent example of the limitations of Typescript's duck-typing inference:

TypeScript code:
interface BaseGlass {beverage: string};
interface WineGlass extends BaseGlass {beer: false};
interface BeerGlass extends BaseGlass {beer: true};

// Compiles just fine, each code path creates either a 100% WineGlass or a 100% BeerGlass, and 'thirst' can be either
let thirst : WineGlass | BeerGlass = 
    (Math.random() > 0.5) ?
      { beverage: 'test', beer: false } : { beverage: 'test', beer: true };
   
// Does not compile, because it can't "merge" the two interfaces and figure out out that the expression will satisfy either one
let thirst2 : WineGlass | BeerGlass = 
      { beverage: 'test', beer: (Math.random() > 0.5) ? true : false };


// Well if it can figure out that true + false = boolean, why can't it apply that reasoning 
// to a record containing one such field? But there isn't a good place to stop that reasoning:

// The following code is also correct, but in order to realize that all possible results are valid, 
// the compiler would need to basically analyze the code paths at the value level (dependent typing)

interface ChessPiece { name : string };
interface Bishop extends ChessPiece { diagonal : true, straight : false };
interface Rook extends ChessPiece { diagonal : false, straight : true };

let b = (Math.random() > 0.5);

// does not compile, same reason as thirst2
let piece : Bishop | Rook = { name : "rook or bishop", diagonal : b, straight : !b };

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer
I'm losing my mind here. The earlier example does work from compiler version 3.5 and onwards, even with only type Glass = BeerGlass | WineGlass and beer: true/false on the interfaces.. I could've sworn I tried it earlier and that it failed, but it works. The change is named very explicitly in the 3.5 documentation: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking under "Smarter union type checking". The reason it works is because true and false are all the possible values of boolean, so the compiler is essentially able to turn { beer: true } | { beer: false } into { beer: boolean }. It can do the same thing with any other enumerated type when it can see that you've exhaustively enumerated every possible value.

Am I still missing something? Try switching between 3.3.3 and 3.5.1 here: https://www.typescriptlang.org/play...QsWoOxKnuHunWQA. It works in 3.5.1 and on, and says "type 'boolean' is not assignable to type 'false'" in 3.3.3.

Red Mike posted:

Union of two interfaces does not lead to each property being a union of the equivalent properties from each interface; that would be insane and counter-intuitive. It would have no ability to figure out what the correct types are half the time.

It does when the fields exist in all the unioned types, so { type: 'A' } | { type: 'B' } | { type: 'C' } is equivalent to { type: 'A' | 'B' | 'C' }.
Or even this: { foo: 'Foo', bar: 'Bar' } | { foo: 'Bar', bar: 'Foo' } is equivalent to { foo: 'Foo' | 'Bar', bar: 'Foo' | 'Bar' }.

Red Mike posted:

This is literally what you're trying to do, which is to have the property "beer" be a union of true and false in the base type that you can use to store the object in. But instead of making the property "beer" a union, you're trying to make a union of two object types that both define a property with different types.

[...]

e: corrected the example which just also highlights why it's a bad idea. You can't avoid defining beer: true or beer: false because an interface is not the same thing as a class.

I believe the idea was to have "beer" be a union of true/false -- which is exactly equivalent to the boolean type -- only on the union type Glass and not on the base type BaseGlass. I don't see anything wrong with defining the type that way.

NihilCredo posted:

TypeScript code:
// Does not compile, because it can't "merge" the two interfaces and figure out out that the expression will satisfy either one
let thirst2 : WineGlass | BeerGlass = 
      { beverage: 'test', beer: (Math.random() > 0.5) ? true : false };

// does not compile, same reason as thirst2
let piece : Bishop | Rook = { name : "rook or bishop", diagonal : b, straight : !b };

thirst2 does compile in version 3.5+. The piece variable indeed doesn't work in any version. I assume it's because while the compiler now knows that boolean is equivalent to true | false, it doesn't know that !true is equivalent to false and !false is equivalent to true. That's something that could theoretically be added.

Edit: Scratch that last part. The compiler actually does know that !true is false and vice versa, so the type of const x = !true is false, not boolean. As you said the reason it can't tell that the diagonal/straight variables are guaranteed to be correct is deeper, and would require something like dependent types.

LOOK I AM A TURTLE fucked around with this message at 16:27 on Sep 26, 2022

NihilCredo
Jun 6, 2011

iram omni possibili modo preme:
plus una illa te diffamabit, quam multæ virtutes commendabunt

LOOK I AM A TURTLE posted:

thirst2 does compile in version 3.5+. The piece variable indeed doesn't work in any version. I assume it's because while the compiler now knows that boolean is equivalent to true | false, it doesn't know that !true is equivalent to false and !false is equivalent to true. That's something that could theoretically be added.

Interesting, they did add some better handling apparently.

However, it's not about the 'not' operator. This still doesn't compile in 4.8.2:

TypeScript code:
interface ChessPiece { name : string };
interface Queen extends ChessPiece { diagonal : true, straight : true };
interface Knight extends ChessPiece { diagonal : false, straight : false };

let b = (Math.random() > 0.5);

let piece2 : Queen | Knight = { name : "knight or queen", diagonal : b, straight : b };

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer

NihilCredo posted:

However, it's not about the 'not' operator. This still doesn't compile in 4.8.2:

Yeah, you're right. The compiler does in fact know that !true is false and !false is true, but it's not enough.

Red Mike
Jul 11, 2011

LOOK I AM A TURTLE posted:

I'm losing my mind here. The earlier example does work from compiler version 3.5 and onwards, even with only type Glass = BeerGlass | WineGlass and beer: true/false on the interfaces.. I could've sworn I tried it earlier and that it failed, but it works. The change is named very explicitly in the 3.5 documentation: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking under "Smarter union type checking". The reason it works is because true and false are all the possible values of boolean, so the compiler is essentially able to turn { beer: true } | { beer: false } into { beer: boolean }. It can do the same thing with any other enumerated type when it can see that you've exhaustively enumerated every possible value.

Am I still missing something? Try switching between 3.3.3 and 3.5.1 here: https://www.typescriptlang.org/play...QsWoOxKnuHunWQA. It works in 3.5.1 and on, and says "type 'boolean' is not assignable to type 'false'" in 3.3.3.

It does when the fields exist in all the unioned types, so { type: 'A' } | { type: 'B' } | { type: 'C' } is equivalent to { type: 'A' | 'B' | 'C' }.
Or even this: { foo: 'Foo', bar: 'Bar' } | { foo: 'Bar', bar: 'Foo' } is equivalent to { foo: 'Foo' | 'Bar', bar: 'Foo' | 'Bar' }.

I believe the idea was to have "beer" be a union of true/false -- which is exactly equivalent to the boolean type -- only on the union type Glass and not on the base type BaseGlass. I don't see anything wrong with defining the type that way.

My bad, it looks like they fixed the boolean/all values specifically enumerated edge cases. That doesn't change that the entire approach is trying to do what is basically classes and sub-classing, except it's doing it via interfaces (duck-typing) and unions. Sure, you can do it, but that's not the right tool for the job. Highlighted by the fact that they deliberately had to add handling for these cases and are unable to handle the general case. Because it's the wrong tool for the job.

Don't get me wrong, in TS all too often you'll end up having to use that particular tool, because an interface is what you have to use (or what a library provides, or what a tool expects, etc). But it's not a slight against the language that it's letting you hammer in a screw but it doesn't work that well all things considered. The closest thing to a slight is that interfaces/unions are too readily available for things that you should use classes/other types for, but that's vague and probably not solvable.

e: I'll be honest, more of a horror is literal types altogether because they're a hacky fix that's been taken to a ridiculous extreme. There just shouldn't be a way to define an interface as "type false", only "type boolean".

Volte
Oct 4, 2004

woosh woosh
I assume that's because there's no way to even write the type of 'diagonal' or 'straight' independently in the union context, so they can't be unified independently and it falls back to the "all operations must be valid on all union branches" thing, so 'b' would have to be assignable to both Queen and Knight. It's not enough to say that they are both booleans, because then true/false and false/true would be valid.

Volte fucked around with this message at 16:42 on Sep 26, 2022

Absurd Alhazred
Mar 27, 2010

by Athanatos

LOOK I AM A TURTLE posted:

TypeScript code:
interface Foot { name: 'Your foot', toeCount: number; }
interface Enemy { name: string; }

function shoot(enemy: Enemy): void {
  console.log(`${enemy.name} has been shot!`);
}

function dumbFunction(_enemy: Enemy): Enemy {
  const foot: Foot = { name: 'Your foot', toeCount: 5 };
  return foot as any;
}

let enemy: Enemy = { name: 'PHP' };
enemy = dumbFunction(enemy);
shoot(enemy);
You can guess what the output is.

... no? Sorry, I haven't even touched Javascript in years, I'm not sure I'm seeing the problem. Will it not print out: "Your foot has been shot!"?

pokeyman
Nov 26, 2006

That elephant ate my entire platoon.

Red Mike posted:

My bad, it looks like they fixed the boolean/all values specifically enumerated edge cases. That doesn't change that the entire approach is trying to do what is basically classes and sub-classing, except it's doing it via interfaces (duck-typing) and unions. Sure, you can do it, but that's not the right tool for the job. Highlighted by the fact that they deliberately had to add handling for these cases and are unable to handle the general case. Because it's the wrong tool for the job.

Don't get me wrong, in TS all too often you'll end up having to use that particular tool, because an interface is what you have to use (or what a library provides, or what a tool expects, etc). But it's not a slight against the language that it's letting you hammer in a screw but it doesn't work that well all things considered. The closest thing to a slight is that interfaces/unions are too readily available for things that you should use classes/other types for, but that's vague and probably not solvable.

e: I'll be honest, more of a horror is literal types altogether because they're a hacky fix that's been taken to a ridiculous extreme. There just shouldn't be a way to define an interface as "type false", only "type boolean".

It's been awhile since I spent serious time in TypeScript (or JavaScript), but promoting class inheritance and decrying literal types seems to entirely miss the point of TypeScript. Maybe your problem isn't wrong tool for the job, it’s that you've actually got a different job on your hands?

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer

Red Mike posted:

My bad, it looks like they fixed the boolean/all values specifically enumerated edge cases. That doesn't change that the entire approach is trying to do what is basically classes and sub-classing, except it's doing it via interfaces (duck-typing) and unions. Sure, you can do it, but that's not the right tool for the job. Highlighted by the fact that they deliberately had to add handling for these cases and are unable to handle the general case. Because it's the wrong tool for the job.

I'd say it's quite debatable if it's the right or wrong tool. Many languages don't have classes at all, so their only way to implement polymorphism is with algebraic data types. TypeScript offers both, but personally I prefer discriminated unions to classes in many cases. We could go into a big debate about the pros and cons of OOP here, but all I'll say is that there's a reason the industry has started to move away from subtyping in the last decade or so. Subtyping has many things going for it, but it also has a number of disadvantages.

Also, I know this is a very pedantic point, but TS interfaces are structurally typed, not duck-typed. You can turn any object into an ad-hoc type, but you can't access data on an object unless you said that it's there. If duck typing is "if it quacks like a duck", structural typing is more like "a duck is whatever I say it is".

Absurd Alhazred posted:

... no? Sorry, I haven't even touched Javascript in years, I'm not sure I'm seeing the problem. Will it not print out: "Your foot has been shot!"?

It will, but I wanted to shoot my enemies, not my own foot. The shoot function was not supposed to be able to take a foot as input, but it's embarrassingly easy to do it anyway. The layer of safety added by TypeScript is very thin compared to languages that don't have (almost) full type erasure at runtime.

NihilCredo
Jun 6, 2011

iram omni possibili modo preme:
plus una illa te diffamabit, quam multæ virtutes commendabunt

LOOK I AM A TURTLE posted:

It will, but I wanted to shoot my enemies, not my own foot. The shoot function was not supposed to be able to take a foot as input, but it's embarrassingly easy to do it anyway. The layer of safety added by TypeScript is very thin compared to languages that don't have (almost) full type erasure at runtime.

My man you literally wrote "as any" in your example. It's hard to complain that 'the layer of safety is very thin' when you specifically used the keyword whose only and entire purpose is to disable the type checker.

Volte
Oct 4, 2004

woosh woosh

LOOK I AM A TURTLE posted:

It will, but I wanted to shoot my enemies, not my own foot. The shoot function was not supposed to be able to take a foot as input, but it's embarrassingly easy to do it anyway.
You're thinking about it from a nominal typing point of view, but that doesn't apply in Typescript. It didn't take a Foot as an input; it took an Enemy as an input, which is another way of saying "an object that has a name". You didn't even have to use "as any" there. It's not a violation of the type contract because Foot is a structural subtype of Enemy.

And frankly any example that does use "as any" is a poor indictment of the type system, as it is explicitly designed to escape from the constraints type system.

Volte fucked around with this message at 17:48 on Sep 26, 2022

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer

Volte posted:

It's not a violation of the type contract because Foot is a structural subtype of Enemy.

Woops, you're right. I should've put some other field on Enemy to differentiate them.

Anyway, I know it wasn't a very impressive example. I was just trying to point out how the type system of TypeScript is quite different from that of languages like C# and Java, where it's practically impossible to put a string inside an int or whatever. I'm actually a big fan of TypeScript, but I've also had a few too many experiences of my variables not containing what I think they do, because that "as any" part was hidden in some deep dark corner of the code.

Absurd Alhazred
Mar 27, 2010

by Athanatos
So in C++ if I define:

Cpp posted:

struct JustAString
{
std::string String;
};

struct AStringAndAnotherThing
{
std::string String;
int Another;
}

then a function that accepts JustAString&s would refuse to compile with AStringAndAnotherThing&, even though theoretically I could have also written:

quote:

struct AStringAndAnotherThing : public JustAString
{
int Another;
}
and then it would be fine. You're saying TypeScript doesn't see a difference between these two?

Volte
Oct 4, 2004

woosh woosh

Absurd Alhazred posted:

So in C++ if I define:

then a function that accepts JustAString&s would refuse to compile with AStringAndAnotherThing&, even though theoretically I could have also written:

and then it would be fine. You're saying TypeScript doesn't see a difference between these two?
It's not that it doesn't see a difference, just that they are related. In TypeScript, the type is the structure. The name you give it is just an alias for that particular structure. Any two aliases of the same structure are the same type. And due to structural subtyping, the type structure aliased by AStringAndAnotherThing is a subtype of type structure aliased by JustAString, so you can use value that conforms to AStringAndAnotherType whenever a value that conforms to JustAString is expected (but not vice versa).

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer

Absurd Alhazred posted:

So in C++ if I define:

then a function that accepts JustAString&s would refuse to compile with AStringAndAnotherThing&, even though theoretically I could have also written:

and then it would be fine. You're saying TypeScript doesn't see a difference between these two?

TypeScript code:
im dumb
Edit: this whole post was wrong. I should stop posting.

LOOK I AM A TURTLE fucked around with this message at 19:27 on Sep 26, 2022

Volte
Oct 4, 2004

woosh woosh

LOOK I AM A TURTLE posted:

It does enforce nomimal typing for class objects, but not for "plain objects".
This isn't the case. Typescript doesn't distinguish between classes and other types, and your first example would compile fine if you passed the correct number of arguments to the B constructor. The explicit inheritance mechanic is part of JavaScript's class system and is necessary to maintain that inheritance relationship at runtime, but the same structural subtyping rules apply regardless of inheritance.

edit: :rip:

Absurd Alhazred
Mar 27, 2010

by Athanatos
If TypeScript doesn't change its behavior if I change something then I'm going to call it "it doesn't see it". :shrug:

Eugh, I just hope I never get pushed into webdev.

LOOK I AM A TURTLE
May 22, 2003

"I'm actually a tortoise."
Grimey Drawer

Volte posted:

This isn't the case. Typescript doesn't distinguish between classes and other types, and your first example would compile fine if you passed the correct number of arguments to the B constructor. The explicit inheritance mechanic is part of JavaScript's class system and is necessary to maintain that inheritance relationship at runtime, but the same structural subtyping rules apply regardless of inheritance.

edit: :rip:

Yeah, I was way off. It just goes to show how little I use classes in TS.

Zopotantor
Feb 24, 2013

...und ist er drin dann lassen wir ihn niemals wieder raus...

FlapYoJacks posted:

Python is the best non-compiled language there is. Rust is the best compiled language.

You misspelled Lisp.

Twice.

:colbert:

Volte
Oct 4, 2004

woosh woosh

Absurd Alhazred posted:

If TypeScript doesn't change its behavior if I change something then I'm going to call it "it doesn't see it". :shrug:

Eugh, I just hope I never get pushed into webdev.
It's the exact same relationship as in C++ if the inheritance was specified. It's not that it can't see the difference between the base class and the subclass, it's just that they are both compatible with that particular parameter. If you tried to implicitly cast the other way, both languages would fail to do it for the same reason.

Armauk
Jun 23, 2021


Canine Blues Arooo posted:

Does Rust have any good GUI Frameworks?

Not yet. Rust is excellent for command-line programs, at the moment.

Absurd Alhazred
Mar 27, 2010

by Athanatos

Volte posted:

It's the exact same relationship as in C++ if the inheritance was specified. It's not that it can't see the difference between the base class and the subclass, it's just that they are both compatible with that particular parameter. If you tried to implicitly cast the other way, both languages would fail to do it for the same reason.

That's exactly it, though: I don't want my compiler/interpreter helpfully finding these underlying structures for me, I want it to raise the alarm that I passed something in that wasn't explicitly allowed.

Volte
Oct 4, 2004

woosh woosh

Absurd Alhazred posted:

That's exactly it, though: I don't want my compiler/interpreter helpfully finding these underlying structures for me, I want it to raise the alarm that I passed something in that wasn't explicitly allowed.
You are explicitly allowing it by defining the structure that way. It's not a helpful compiler shortcut, it's a fundamentally different kind of type system. The name of a type simply does not matter beyond documentation. You don't even need to use the name of the type to reference it, you can directly write out the structure. If you actually want to enforce name identity, you'd have to build that into the structure of the type.

There are some ways to enforce nominal typing (for example) but I wouldn't say it would be desirable all the time.

Absurd Alhazred
Mar 27, 2010

by Athanatos

Volte posted:

You are explicitly allowing it by defining the structure that way. It's not a helpful compiler shortcut, it's a fundamentally different kind of type system. The name of a type simply does not matter beyond documentation. You don't even need to use the name of the type to reference it, you can directly write out the structure. If you actually want to enforce name identity, you'd have to build that into the structure of the type.

There are some ways to enforce nominal typing (for example) but I wouldn't say it would be desirable all the time.

You keep using the word "explicit". I don't think it means what you think it means.

Phobeste
Apr 9, 2006

never, like, count out Touchdown Tom, man
Typescript is a structural-by-default typing system, the opposite of c++. It is like if all c++ functions were templates you provide various levels of constraint to. It's not that complicated

Volte
Oct 4, 2004

woosh woosh

Absurd Alhazred posted:

You keep using the word "explicit". I don't think it means what you think it means.
If you define a function that says "I will accept any object that has a string called 'name'" and then you define an object that has a string called 'name', that's being pretty explicit about the conformance of one thing to the other.

cheetah7071
Oct 20, 2010

honk honk
College Slice
it seems reasonable enough for me? Like even in C++ you can use typedefs to get pretty similar and useful behavior. A size_t and uint64_t are the same thing other than the name, but listing "size_t" in the function signature communicates to the user how the function is supposed to be used, even if it doesn't completely restrict them from using it wrongly

Absurd Alhazred
Mar 27, 2010

by Athanatos

Volte posted:

If you define a function that says "I will accept any object that has a string called 'name'" and then you define an object that has a string called 'name', that's being pretty explicit about the conformance of one thing to the other.

But that's not what I'm doing. I'm saying "I accept SpecificallyThisObject", that happens to have a string in it. I don't want it to accept "ThatEntirelyDifferentObjectThatHappensToStartWithAString" without me explicitly enabling that behavior somehow because it detects that there's this structural similarity and just "helpfully" does that for me.

Phobeste posted:

Typescript is a structural-by-default typing system, the opposite of c++. It is like if all c++ functions were templates you provide various levels of constraint to. It's not that complicated

Yeah. That's why I said that TypeScript doesn't see a difference between things that C++ does, so I'm not sure why this argument is continuing. If I didn't explicitly state something and it happens, then it happened implicitly. I don't care why it's happening implicitly.

Phobeste
Apr 9, 2006

never, like, count out Touchdown Tom, man
No. As with a c++ template, you state "I accept specifically an object that has this shape" that explicitly has a string in it with explicitly that name. The compiler "detects" (in scarequotes because it's really not any different from "detecting" that an object has the same class definition identity) that an argument matches the structure of what you've explicitly asked for, and allows it.

This is not any less strict or less sound than nominative typing. It is just different. There's not an argument, you're just incorrect.

Volte
Oct 4, 2004

woosh woosh

Absurd Alhazred posted:

But that's not what I'm doing. I'm saying "I accept SpecificallyThisObject", that happens to have a string in it. I don't want it to accept "ThatEntirelyDifferentObjectThatHappensToStartWithAString" without me explicitly enabling that behavior somehow because it detects that there's this structural similarity and just "helpfully" does that for me.
If you don't want it to accept that then you should not define your intended interface in a way that unambiguously has a subtype relation with it. You claim you're saying "I accept SpecificallyThisObject" but in TypeScript, you are not saying that because it can't be said. You can encode a nominal identity inside the structure (like { name: string, kind: "SpecificallyThisObject" }) if you want, but at the language level, the name of the type is not important to type checking, full stop.

Absurd Alhazred
Mar 27, 2010

by Athanatos

Phobeste posted:

No. As with a c++ template, you state "I accept specifically an object that has this shape" that explicitly has a string in it with explicitly that name. The compiler "detects" (in scarequotes because it's really not any different from "detecting" that an object has the same class definition identity) that an argument matches the structure of what you've explicitly asked for, and allows it.

This is not any less strict or less sound than nominative typing. It is just different. There's not an argument, you're just incorrect.

This is the "if you access our website you have signed the EULA" rider of type systems.

Volte posted:

If you don't want it to accept that then you should not define your intended interface in a way that unambiguously has a subtype relation with it. You claim you're saying "I accept SpecificallyThisObject" but in TypeScript, you are not saying that because it can't be said. You can encode a nominal identity inside the structure (like { name: string, kind: "SpecificallyThisObject" }) if you want, but at the language level, the name of the type is not important to type checking, full stop.

So what I said:

Absurd Alhazred posted:

So in C++ if I define:

then a function that accepts JustAString&s would refuse to compile with AStringAndAnotherThing&, even though theoretically I could have also written:

and then it would be fine. You're saying TypeScript doesn't see a difference between these two?

is correct. Why are still arguing with me?

Absurd Alhazred fucked around with this message at 21:09 on Sep 26, 2022

Volte
Oct 4, 2004

woosh woosh
It's incorrect because you can't pass JustAString in a place where AStringAndAnotherThing is expected. If it didn't see a difference between them, that would obviously not be the case. There's a big difference between "A and B are both acceptable" and "A and B are indistinguishable".

edit: I think I misunderstood by what you meant by "these two". I thought you meant the types, not the two examples. I guess we're arguing for nothing.

Adbot
ADBOT LOVES YOU

Absurd Alhazred
Mar 27, 2010

by Athanatos

Volte posted:

It's incorrect because you can't pass JustAString in a place where AStringAndAnotherThing is expected. If it didn't see a difference between them, that would obviously not be the case. There's a big difference between "A and B are both acceptable" and "A and B are indistinguishable".

edit: I think I misunderstood by what you meant by "these two". I thought you meant the types, not the two examples. I guess we're arguing for nothing.

I meant between the two instances of AStringAndAnotherThing. The one that explicitly inherits from JustAString vs. the one that just structurally contains a prefix equivalent to JustAString. C++ will reject the latter up-conversion unless I use something explicit like reinterpret_cast (which is not recommended because I want it to force me to think about why I did or didn't perform this inheritance).

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply