2 ms·
> I'm of the functional persuasion, yet I've found that classes are the ony way to write TypeScript that fits on your screen at all. Huh? I’m of the functional
by eyelidlessness 4y ago
> I'm of the functional persuasion, yet I've found that classes are the ony way to write TypeScript that fits on your screen at all.
Huh? I’m of the functional persuasion too, and I use classes in TS too, but for strategic reasons (well defined value objects are easier to reason about than duck typed POJOs, and they perform better too). But I’ve never found them more space-dense than the equivalent function-only code. Often quite the opposite, as so many functions’ return types can be fully inferred. Which of course, this is how you get ~~ants~~ duck typed POJOs, but you can’t have explicit type defs without explicit defining them somewhere, and of course the syntax that collocates the field type and its value is more dense than the syntax which is wholly incompatible with that concept.
> handling of default property values and "definedness" differs
The only difference is that TS provides a fully optional shorthand for assigning both the type and value in constructor arguments. The actual behavior isn’t any different. This:
class Foo {
constructor(readonly bar: number) {}
}
is identical to:
class Foo {
readonly bar: number;
constructor(bar: number) {
this.bar = bar;
}
}
is identical to:
class Foo {
bar;
constructor(bar) {
this.bar = bar;
}
}
And this type error:
class Foo {
readonly bar: number;
constructor(bar: number) {}
}
is identical to this type error, just caught sooner:
class Foo {
bar;
constructor(bar: number) {}
}
const foo = new Foo();
foo.bar.toFixed(2);
> Sure, the constructor signature may change in a subclass (why not disallow incompatible constructor overrides, given incompatible property/method signatures are already disallowed?)
Because the compatibility is checked on the `super` call which is required both at compile time and runtime, and because many use cases for subclasses are impossible or even invalid without different construction contracts.
I know there are many strong feelings about examples like this being “wrong”, but it’s a common enough inheritance example to illustrate the point:
class Square extends Rectangle {
constructor(/* ? */) {}
}
You cannot satisfy both Square and Rectangle with the same constructor arguments. This of course bolsters the point that this inheritance model is “wrong”, but it’s exactly right according to the domain, and the equivalent functional code to calculate eg area would similarly have to be either polymorphic over different shapes or expect a single shape constructed with different parameters.
> this.constructor
You got this one right, and it’s worse than you describe because of the weird rules for where you can’t have type parameters or explicit `this` types. The workaround is to use a static factory method, but it’s a shitty workaround with a lot of ceremony to do something that TS generally does well: model the types of real world JS code.
> Also crap like not being able to have a question mark and a default value in positional arguments so you gotta add `|undefined` there. Even the stuff it adds on top of JS is poorly thought out.
Ima help you out! Assuming your default satisfies the non-undefined type, you can just skip the union, it’s implied. This:
const foo = (bar: Bar = someBarSatisfyingValue) => {};
is identical to this:
const foo = (bar: Bar | undefined = someBarSatisfyingValue) => {};
is identical to this:
const foo = (bar?: Bar) => {
bar = bar ?? someBarSatisfyingValue;
};
- kuramitropolis 4y ago>you can’t have explicit type defs without explicit defining them somewhere Sure, as long as I have to define them once and exactly once. Not always possible, as in the case of simple, garden-variety keyword arguments. // foo is required, bar has default, baz is optional type POJO = { foo: number, bar: number, baz?: number } function myFn ({ foo, bar = 3, baz }: POJO = {}) { // oh wait... function myFn ({ foo, bar, baz }: POJO = { bar: 3 }) { // oh wait... function myFn ({ foo, bar, baz }: Partial<POJO> = {}) { bar ??= 3 if (foo === undefined) throw new Error("type safety") // ...oh. // maybe?: function myFn ( foo, bar, baz }: POJO = { foo: undefined as never, bar: 3 }) { // try :D Technically the destructuring and the type declaration are completely separate things ofc (that just happen to look about the same because they're isomorphic but that's a watchlist word). But... it doesn't even try to infer the type of an untyped destructuring - even if it's a local function used only once! >well defined value objects are easier to reason about than duck typed POJOs, and they perform better too Long live those! class BaseValueObject { constructor (values: Partial<this> // oh wait... > Assuming your default satisfies the non-undefined type, you can just skip the union, it’s implied type Foo = { defaultBar?: Bar } function main (foo: Foo, bar?: Bar = foo.defaultBar) { // oh wait... parameter can't have question mark an initializer function main (foo: Foo, bar?: Bar|undefined = foo.defaultBar) { // this works but is silly and scaryish >The only difference is that TS provides a fully optional shorthand for assigning both the type and value in constructor arguments. The actual behavior isn’t any different That's what a sane person would assume, no? Well, allow me to disappoint you (like that ever needs permission): $ node > Object.getOwnPropertyNames(new class Foo { a }) [ 'a' ] $ npx ts-node > Object.getOwnPropertyNames(new class Foo { a: any }) [] Probably because it compiles them to a pre-standard, ES5-compatible class implementation based on good ol' `Foo.prototype`. And since they've already handled them one way, they can't become spec-compliant without breaking backwards compatibility. The other place where this shines through particularly egregiously is the support of ESM static import/export. Everybody's build tools been compiling that back down to CJS so hard that Node.js 16+ introduced intentional incompatibilities between CJS and ESM modes just to get people to finally switch to the standards-compliant module system. So you end up in a situation where the library is written in TypeScript with ESM syntax but the only available browser build is a CJS blob which completely defeats the main touted benefit of static imports/exports, namely dead code elimination... So you decide what the hell, let's switch TSC to ESM and moduleResolution node16, and end up having to use something like https://github.com/antongolub/tsc-esm-fix https://github.com/antongolub/tsc-esm-fix because the only allowed fix for TSC doing the wrong thing is at the completely wrong level - https://www.typescriptlang.org/docs/handbook/esm-node.html https://www.typescriptlang.org/docs/handbook/esm-node.html - if you don't see what's wrong with that, you're one of today's lucky 10000...