3 ms·
>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,
by 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...
- eyelidlessness 4y ago> Not always possible, as in the case of simple, garden-variety keyword arguments. // foo is required, bar has default, baz is optional All of your examples are correctly identified by TS as type errors, because they all have a default argument which will never bind `foo`. True in JS as well as TS. Consider the untyped code, with some access to a required `foo`: function myFn ({ foo, bar = 3, baz } = {}) { return bar + (baz ?? foo); } myFn(); // NaN Your function shouldn’t supply a default argument, because if the foo property is required the object containing it has to be too. It should instead supply defaults to the properties in it. This is closer to what you seem to want: function myFn (pojo: POJO) { const { foo, bar = 3, baz } = pojo; // foo is required, bar has default, baz is optional return bar + (baz ?? foo); } myFn(); // Compile error myFn({}); // Compile error myFn({ foo: 2 }); // 5 myFn({ foo: 2, bar: 4 }); // 6 myFn({ foo: 2, bar: 4, baz: 6 }); // 10 myFn({ foo: 2, baz: 6 }); // 9 > But... it doesn't even try to infer the type of an untyped destructuring - even if it's a local function used only once! Yes, it does, if the thing you’re destructuring is typed. It doesn’t infer from usage, and while that sounds nice and some languages have it, it's fairly uncommon. > oh wait... parameter can't have question mark an initializer Yeah. It can’t. But if you applied what I suggested, it compiles and has the type you expect without adding undefined: type Foo = { defaultBar?: Bar } function main (foo: Foo, bar: Bar = foo.defaultBar) {} type Main = typeof Main; // function main (foo: Foo, bar?: Bar | undefined) {} > 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. You’re partly right. I’m on mobile so I can’t dig into the failure but type checker seems to be crashing or getting stuck due to the confusing syntax. If you make it more clear by putting parentheses around the class expression, you still won’t get any compiler errors because it’s constructed with no arguments, and a is implicitly assigned undefined which satisfies any. If you then give the a property a non-any/unknown type you’ll get a compile error because a wasn’t assigned. It’s weird that even newer compile targets don’t get an assigned a: undefined at runtime, and definitely qualifies as a compiler bug (you should file it! I’ll add what I’ve learned!). It certainly does if you actually assign anything to a during construction. > 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. This is factually false. CJS is fundamentally incompatible with ESM, and has been since day 1. They shipped it incompatible from at least Node 12 because there’s no way to make it compatible. ESM is fundamentally async, and CJS is fundamentally blocking. ESM imports are “live”, CJS are static values at the time you call require. They have fundamentally different module resolution algorithms. All of this has been documented in Node also since at least v12, and has been spec compliant (notwithstanding since fixed bugs) the whole time. There are definitely valid gripes about how TS has supported ESM though, particularly in terms of file extensions. Thankfully they’re actively working to address that now.