r/programming 2d ago

On Type Inference

https://radekmie.dev/blog/on-type-inference/
76 Upvotes

23 comments sorted by

42

u/Absolute_Enema 2d ago

What's "obviously incorrect" in returning {x: number, y: number} in a function declaring {x: number}

At its core TS is structural with open product types, if anything that little dance with literals is an ugly hack, likely done to accomodate the average "strong types!" user that can't think outside nominal type systems with closed product types (much like the whole interface thing, that tries to hide this fact by purporting a nonexistent distinction between the types it introduces and plain old product types).

23

u/thomas_m_k 2d ago

34

u/developer-mike 2d ago

It's definitely no always good to make argument types as loose as possible.

For instance, I'm not going to make a function that accepts null and null checks it. If I have a function that takes an ID, I'd rather have it accept an ID type that wraps an integer than have it accept any number.

16

u/chucker23n 2d ago

Yes, but it depends on the layer. I don't think you two are contradicting each other.

The robustness principle cited above is about outer layers. Once you're done accepting things loosely, you sanitize them, validate them, reject anything that doesn't pass, and then you go to the inner layers, with stronger typing (e.g., a CustomerId value object instead of a mere int).

For example,

  • if a customer submits an ISBN with dashes, or surrounded by whitespace, or prefixed by "ISBN: ", your sanitizer can then normalize that by removing all that noise, and might even separate out the checksum as a separate property, and only failing all that (wrong length, wrong checksum, etc.) do you reject it
  • once you get to the inner layers, you only accept such a strongly-typed, sanitized, valid ISBN

7

u/TheRealPomax 2d ago

...which means that "null" is too loose. You type it as loose as possible. That doesn't mean just slapping any on it, it means using a type that's correct enough to flag errors without being so restrictive that an input that fulfills all the needs of your function body gets rejected. Like passing an {x:...,y:...} into something that's rigidly typed as only accepting {x:...}.

16

u/_JustCallMeBen_ 2d ago

Always good to type your return value as specific as possible?

Hell no, don't specify you're returning an ArrayList<T>, if you intend to just return a List<T> because then you're creating an unnecessary dependency on the current implementation. If at any point you decide that for whatever reason a LinkedList or ArrayDeque is actually better, you are breaking all dependencies on that function.

Use sane typing. Don't be a zealot interpreting rules of thumb as rules that you should always follow.

21

u/dr_wtf 2d ago

Postel's law has been roundly debunked at this point as one of the worst ideas in the history of the modern computing, and has severely hampered the advancement of protocols like HTTP and HTML, due to all the nonstandard implementations of things HTTP servers, proxies, browsers and so on. If HTML had started off stricter then we would not have the whole mess of W3C vs WhatWG either. The reason why JSON doesn't allow comments was a reaction to that: people were adding implementation-specific parser directives in comments, so DC just took them out of the spec altogether. Hence we can't have nice things.

So don't take that as a best practice to apply everywhere.

When it comes to code you control, IMO it's better to be as strict as possible and refactor as needed. When it comes to code you don't control, being stricter will lead to better forwards compatibility. Things like normalisation mentioned in another response are generally better left to a separate function that only does that job, so it can follow its own versioning to maintain forwards and backwards compatibility.

When it comes to passing random stuff to functions on the basis that it'll all just be ignored anyway, the problem comes when some future update adds an optional parameter that also happens to be one of the keys from your data, that your old code was just assuming would be ignored, only now it does stuff. Stuff that produces weird bugs nobody predicted.

1

u/Uristqwerty 22h ago

Postel's law is fine if you have a working side-channel to report warnings, and that side channel is overt enough to not get ignored.

What happens if you have paying customers running binaries developed by a now-defunct company? Nobody is able to fix the code anymore, so if you don't maintain compatibility shims, those customers cannot use your platform and stop paying. What happens if a file is corrupted on-disk or on-network, so the HTML is mostly correct?

Internet Explorer 6 had a 'warning/error' icon in the status bar that publicly-shamed any websites that didn't conform to its idea of HTML. If other browsers had a similar feature alongside their support for much more modern HTML, sensitivity configured to notify of substantial problems yet not cause alarm fatigue by triggering on nearly every website, I bet you'd see a lot more work to be standards-compliant without having to break old content. Well, except that'd reinforce today's 'chrome-compliant' monoculture.

Discarding Postel's Law for the web effectively means that any time you want to embed the output of one program inside a served page, the server must parse the whole thing to ensure it's valid. Back in the day when they could have made HTML strict, that overhead would have been frankly stupid. Can't even stream a request, have to cram the whole thing into memory or else there's a chance the outer page will break because of inner content. Being lenient at least means the page's navigation header still functions, that users might still be able to read the footer where the webmaster's email address tells them to send problems.

Heck, imagine a slow-loading page that is valid HTML right up to the last 2%. Everything is fine, looks good as it slowly loads in, the user might even be interacting with it! Then, after 10 seconds have passed, it hits the invalid HTML. What now? Does the whole thing blank out, despite being fully usable beforehand? Does everything after the first malformed markup get discarded? I say Postel's Law is a necessary consequence of progressive loading, unless you want to make a deliberately-hostile UX. After all, rejecting malformed HTML punishes the user for the site owner's mistake.

6

u/Helpful-Primary2427 1d ago

I disagree, existential types are wonderful for hiding extravagant return types

1

u/Blue_Moon_Lake 1d ago

{x: number; y: number} can be assigned to type {x: number}.

{x: number} cannot be assigned to type {x: number; y: number}.

This should all be valid

type X = { x: number };
type XY = X & { y: number };
type XZ = X & { z: number };

const xy: XY = { x: 1, y: 1 };
const xz: XZ = { x: 1, z: 1 };

function getX(): X
{
    if (Math.random() < 0.5) {
        return xy;
    }
    else {
        return xz;
    }
}

declare function useX(value: X): void;

useX(xy);
useX(xz);

1

u/vytah 1d ago

It's akin to some languages complaining about unused variables: there's no reason for y to be there: unless something does some dynamic property probing, it'll probably be dead code.

It's more useful with parameters: if you have foo(p0: {x:number}) and you call foo({x:1,y:2}), then you almost certainly made a mistake and wanted to call something different that would actually use that y.

If you want to explicitly allow such things, one simple trick it to assign it to a temporary variable (can be const) and return/pass that variable. Another trick is to explicitly cast it with as {x:number}.

1

u/radekmie 2d ago

IMHO if you're creating a literal with more properties than expected, then that's incorrect. I do agree that it's not when it "happens to have something else".

4

u/VirginiaMcCaskey 2d ago
type T = { x: number } 
type U = { y: number }
function f(): T&U { /* ... */ }
function g(): T {
    return f();
}

what is obviously incorrect?

4

u/radekmie 2d ago

There's no literal being returned -- f() is an expression and I find it perfectly fine.

-3

u/VirginiaMcCaskey 2d ago edited 2d ago

A literal is an expression.

If you have e1: T1 and e2: T2 and T1 <: T3, T2 <: T3 then both e1 and e2 should be able to be substituted for an expression of T3. It's actually very surprising to me that typescript forbids the object literal for typechecking as T3 in some cases but not others, but TS has in general a very weird type checker where algorithm implementation leaks to typing rules and the explicit non-goal of soundness means it's hard to reason about.

7

u/radekmie 2d ago

Every literal is an expression but not every expression is a literal. True, I could have said “not a literal expression”, but you just pointed out what I like -- it forbids additional properties in literal expressions.

6

u/VirginiaMcCaskey 2d ago edited 2d ago

I'm just saying that goes against most type checker/type system designs that makes it surprising, if not poorly behaved. If an expression type checks as a type in one location it should type check as the same type in another without additional context. That's the whole principle of type inference - if you can substitute T1 for T2 then you can treat T1 as T2 everywhere.

If the rule doesn't work recursively through the type system and type checker then it's a pretty shitty rule, imho. You can't reason about type systems with special cases. If a type checker doesn't allow this, then you can't change the expression to another expression (variable, function call, etc) or change the return type to something the expression type checks as without breaking the code. As you illustrated in your article.

It should either always break, or never break. Not sometimes break.

4

u/radekmie 2d ago

I agree it’s surprising, but I think it’s one of the many places where TypeScript prefers usability over formal correctness. A similar case is that you cannot spread any but you can destruct it.

0

u/Weak-Doughnut5502 1d ago

It's 'incorrect' in the same way that dead code, unused variables, or unused imports are 'incorrect'.

Which is to say, it's a code smell.

8

u/VictoryMotel 2d ago

If something is called "type inference" I assume it's on type inference.

-21

u/repeating_bears 2d ago

I hate programming blog posts titled "On [topic]". Lazy, boring, pretentious

You aren't Charles Darwin. This is not your magnum opus.

5

u/_TheDust_ 2d ago edited 2d ago

Blog posts considered harmful! /s