Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

consider namespace #167

Open
unional opened this issue Feb 15, 2023 · 41 comments
Open

consider namespace #167

unional opened this issue Feb 15, 2023 · 41 comments

Comments

@unional
Copy link

unional commented Feb 15, 2023

namespace is a good way to organize types.

It may worth considering adding type-only namespace to the spec.

In value-level, you can organize values and functions into an object literal:

export const group = {
  func1,
  value2
}

namespace allows you to do the same with types:

export namespace foo {
  export type Param = { ... }
  export type ReturnType = { ... }
}

export function foo(param: foo.Param): foo.ReturnType { ... }

This avoid export pollution so that consumer is not overwhelmed by all the extra types a package exports, when those types are typically not used directly.

@trusktr
Copy link

trusktr commented Sep 20, 2023

I think this is covered by the ideas in

@unional
Copy link
Author

unional commented Sep 20, 2023

I'm not sure if it is covered there. Do you mean:

// a.js
:{
  // I'm adding `export` here as some types might be internal to this scope.
  // The usefulness of such "internal" types are debatable,
  // and ultimately determined by the type engine.
  export namespace A {
    export type Foo { ... }
    export type Boo { ... }
  }
}

// b.js
import: { type A } from './a.js'

const x: A.Foo
const b: A.Boo

@egasimus
Copy link

What of this can't be achieved by putting the code in separate files?

export pollution so that consumer is not overwhelmed by all the extra types a package exports

This sounds like a UI problem 😕

@unional
Copy link
Author

unional commented Sep 21, 2023

This sounds like a UI problem 😕

Yes, I would say it is a DX problem.

Speaking of which, #188 approach may make this a bit more cumbersome:

// foo.ts
export namespace foo {
  export type Options = { ... }
}

export function foo(options: foo.Options) {
}

// boo.ts
import { foo } from './foo.js'

const o: foo.Options = {}

foo(o)

vs

// foo.js
export: {
  export namespace foo {
    export type Options { ... }
  }
}

export function(foo: foo.Options) { ... }

// boo.js
import { foo } from './foo.js'

// implicit import of types?
const o: foo.Options = {}

foo(o)

// or koo.js
import: { type foo } from './foo.js'
import { foo } from './foo.js'

const o: foo.Options = {}

foo(o)

Separating type and code into different files is nice, as in .h and .c file in C,
but supporting co-location of type and code and provide a way to neatly organize them is very beneficial IMO.

@unional
Copy link
Author

unional commented Sep 21, 2023

Another reason for namespace is that flatten list (direct export) is bound to conflict:

import { foo, Options as FooOptions } from './foo.js'
import { boo, Options as BooOptions } from './boo.js'
import { koo, Options as KooOptions } from './koo.js'

@egasimus
Copy link

Another reason for namespace is that flatten list (direct export) is bound to conflict:

I don't understand this example. Isn't that what import * as Foo from './foo.js' is for?

@egasimus
Copy link

supporting co-location of type and code and provide a way to neatly organize them is very beneficial IMO.

Agreed.

@egasimus
Copy link

Speaking of which, #188 approach may make this a bit more cumbersome:

I would say more than a little.

@egasimus
Copy link

@unional

Yes, I would say it is a DX problem.

TBH I would say it's the kind of problem that IDE developers should have the freedom to address.

In order to not create more problems down the road for them, the language should not add something as elaborate as namespaces: a new layer of nesting that doesn't correspond to the filesystem layout of the source code.

For the sake of Occam's razor, my question stands:

What of this can't be achieved by putting the code in separate files?

And, conversely, in what context would you export so many definitions from a single file that you need namespaces to organize them?

@unional
Copy link
Author

unional commented Sep 21, 2023

And, conversely, in what context would you export so many definitions from a single file that you need namespaces to organize them?

e.g.:

😃

Note that ts-toolbelt uses import * as A from './Any/_api to achieve the same thing.

The problem is not just from a single file, it's from a particular package, it can expose hundreds of code, functions, and types.

So namespace (or import * as X) is a nice way to reduce the clutter.

This is a general issue of modules. When we move from global namespace (window.MyCompany.MyPackage.foo) to module (AMD, CommonJS, ESM),
we are effectively "flatten" the exports to a single level.
When the package is small, it works great. But when the package is large, you will start missing the organization benefits of namespaces.

And really, namespaces is just "type literals" as compare to "object literals" (export const x = { a, b, c }) for organization purposes.

@trusktr
Copy link

trusktr commented Sep 21, 2023

I'm not sure if it is covered there. Do you mean:

// ...
import: { type A } from './a.js'
// ...

Not exactly, based on that idea it would be this for the import:

: import { type A } from './a.js'

In your example, with the type comment stripped, it leaves invalid JavaScript:

import;

@trusktr
Copy link

trusktr commented Sep 21, 2023

// foo.js
export: {
  export namespace foo {
    export type Options { ... }
  }
}

Strip the type comments, and that leaves

export

which is not valid JavaScript.

This is valid (strip the whole type comment, and you have empty JS file, which is valid):

// foo.js
:{
  export namespace foo {
    export type Options { ... }
  }
}
// implicit import of types?
const o: foo.Options = {}

This has nothing to do with the comment syntax, this is up to the type system.

// or koo.js
import: { type foo } from './foo.js'
import { foo } from './foo.js'

This would be:

// or koo.js
: import { type foo } from './foo.js'
import { foo } from './foo.js'

but because : already makes it a type feature, TypeScript can adapt to not require type in that case. Like this:

// or koo.js
// type import
: import { foo } from './foo.js'
// real runtime import
import { foo } from './foo.js'

(You do not need both import statements. The runtime import already includes the type import.)

@trusktr
Copy link

trusktr commented Sep 21, 2023

Another reason for namespace is that flatten list (direct export) is bound to conflict:

import { foo, Options as FooOptions } from './foo.js'
import { boo, Options as BooOptions } from './boo.js'
import { koo, Options as KooOptions } from './koo.js'

This doesn't relate to the syntax. TS can do whatever it wants. #188 is only specifying the comment space.

I don't understand this example. Isn't that what import * as Foo from './foo.js' is for?

Indeed, this is preferred over "deprecated" namespace. I never use namespace, and I have no conflicts.

the language should not add something as elaborate as namespaces

With something like #188, the language does not add namespaces.

You can replace this,

:{
  namespace { ... }
}

console.log('this line is actual JavaScript')

with this

:{
  blah blah blah whatever this is just a comment
}

console.log('this line is actual JavaScript')

and it works the same.

It is not the JS language that defines what goes inside the comment in #188, it only defines the comment space.

To explain with another example, this code,

: import { blah } from './blah.js'
import { foo } from './foo.js'

is the same thing as this code in runtime:

: rm -rf /
import { foo } from './foo.js'

Not sure if there's a limitation with :, might need to be double ::, f.e.

:: rm -rf /
import { foo } from './foo.js'

Paste any TypeScript code here, and I'll show the rough equivalent with type comments from 188.

@msadeqhe
Copy link

Not sure if there's a limitation with :, might need to be double ::,

@trusktr, Yes, there's a limitation with :, because it ends with ,, ;, = or {. ::-style comment is a good idea to have instead of :{comment}. Thanks.

@egasimus
Copy link

@unional

This is a general issue of modules. When we move from global namespace (window.MyCompany.MyPackage.foo) to module (AMD, CommonJS, ESM),
we are effectively "flatten" the exports to a single level.
When the package is small, it works great. But when the package is large, you will start missing the organization benefits of namespaces.

I don't really see the distinction. Under the hood, it's all just objects. The packages that you provided as examples already achieve namespacing in a simple and non-confusing way, without the need for a namespace keyword.

export so many definitions from a single file

I can see how these words could be understood in the way you understood them. However this was not their intended meaning. I meant something like "imagine ts-toolbelt was written as a single file".

Perhaps it would do me good to apologize for such an ambiguity - but it's not an ambiguity I introduced, and neither did you. This only drives home my main point: adding constructs to the language without a concern for keeping them all orthogonal to each other, only serves to muddle our ability to communicate unambiguously about specific things.

I'm not arguing against the concept of namespacing. I do think that figuring out ways to achieve new things using the existing means, is always preferable to inventing new, subtly different, ways to achieve what is already doable. For me, a namespace keyword falls in the latter category: it doesn't really map to any concept that is not already trivially representable.

Objects are already public namespaces, closures are already private namespaces, and you can combine those to achieve whatever architecture maps best to your problem domain. I do quite like JavaScript for that sort of simplicity. All a type-level namespace construct could add on top of it would be more assumptions to wrap one's head around (and to work around, if they turn out to be wrong in a given case.)

OTOH, a namespace/module keyword that actually creates and populates an ES Module scope (an existing concept) without the need to create a separate source file, is definitely an idea worth exploring - but it's also quite separate from the concerns of this proposal.

TIL: TS also has module which is... somehow similar to namespace but also different? And then there's also package. Having all three is a bit of a red flag honestly. I haven't looked into the details of what makes them distinct, given that they're erased at runtime in favor of what the file system, module/package system, and bundler do.

@trusktr
Copy link

trusktr commented Sep 22, 2023

Note that namespaces are not erased, but compiled into IIFEs that create objects.

A tool that supports namespace both (1) erases types and (2) adds new JavaScript code into the output. The best is when only type erasure happens, and the code you see is the code you get.

If type-annotations proposal lands, and you rely on a tool to support namespace, your code will not work without a build step! It will not be portable, and it will definitely be forked!

Here are namespaces in plain JS without type annotations for sake of brevity, and if we added type annotations to them, they would all work without a build step (highly portable, copy/paste in any project and it works!):

// namespaces.js

export const Foo = {
  n: 123,
  log() { console.log(this.n) }
}
Foo.n += 123

export const Bar = new (class Bar {
  n = 123
  log() { console.log(this.n) }
  constructor() { this.n += 123 }
})

export class Baz {
  static n = 123
  static log() { console.log(this.n) }
  static { this.n += 123 }
}
// another.js
export let n = 123
export const log = () => console.log(n)
n += 123
import {Foo, Bar, Baz} from './namespaces.js'
import * as Boz from './another.js'

Foo.log()
Bar.log()
Baz.log()
Boz.log()

@unional
Copy link
Author

unional commented Sep 22, 2023

@egasimus I can see your concern and approach.
For me, I focus on DX. Maybe a little too much and not the direct concern to the proposal.

if the proposal flavors #188 and the content inside the :comment is vendor specific, this could be part of that.

Although IMO setting additional guideline and syntax within the :comment block would still be beneficial, or else the tools will diverge and it could become another battleground for "browser war" or "TypeScript vs Flow".

As for import * as X as a mechanism to achieve the same thing as namespace, it works for one specific scenario, but not a general replacement. For example:

// foo.ts
export function foo() {}
export namespace foo {
  export type Options = {}
  export namespace boo {
    export type X = {}
  }
}

// usage.ts
import { foo } from './foo.js'

foo()
const x: foo.Options = {}
const y: foo.boo.X = {}

The reason is that import * as X creates a namespace module (or module namespace, something like that). It is a specific construct.

Note that namespaces are not erased, but compiled into IIFEs that create objects.

@trusktr yes. That is the specific implementation in TypeScript, which is strongly discouraged by the TypeScript team nowadays as we moved to modules.

It was a legacy behavior in TypeScript 1.x where it was still the wild wide west.
It was used to mimic the global namespace behavior:

namespace MyCompany {
  namespace MyProduct {
    const function foo() { ... }
  }
}

MyCompany.MyProduct.foo()

Nowadays namespace is primarily used for organizing types, which is what I propose here.

Thus it is type specific and can be safely erased.

@unional
Copy link
Author

unional commented Sep 22, 2023

Another key use case for namespace: hiding implementation details

export type Foo<T> = SomePreconditionCheck<T> extends true ? Foo.Impl<T> : never

export namespace Foo {
  export type Impl<T> = ...
}

Since all types need to be public, doing Foo.Impl allows those internal utility types to be hidden, or "tuck" under the public type.

@trusktr
Copy link

trusktr commented Sep 23, 2023

// foo.ts
export function foo() {}
export namespace foo {
  export type Options = {}
  export namespace boo {
    export type X = {}
  }
}

// usage.ts
import { foo } from './foo.js'

foo()
const x: foo.Options = {}
const y: foo.boo.X = {}

This already works in TS with type erasure:

// foo.ts
export function foo() {}

export interface foo {
  Options: SomeType
  boo: {
    X: OtherType
  }
}
// usage.ts
import { foo } from './foo.js'

foo()
const x: foo.Options = {} // the same
const y: foo.boo.X = {} // the same

@trusktr
Copy link

trusktr commented Sep 23, 2023

I hadn't tested it, actually usage needs to be:

const x: foo['Options'] = {}
const y: foo['boo']['X'] = {}

But it works! Here's a playground.

@unional
Copy link
Author

unional commented Sep 23, 2023

I hadn't tested it, actually usage needs to be:

Yes. It "works". Quite a hack thou. 😛

Now try this 🤣

// foo.ts
export function foo() {}
export namespace foo {
  export type Options = {}
  export namespace boo {
    export type X = {}
  }
}

export interface foo {
  name: string
  value: string
}

// usage.ts
import { foo } from './foo.js'

foo()
const x: foo.Options = {n: 123}
const y: foo.boo.X = {s: 'asdf'}
const f: foo = {name: 'x', value: 'y'}

https://playground.solidjs.com/anonymous/1233e8c1-14f1-456b-9980-7e8c250a4299

@trusktr
Copy link

trusktr commented Sep 23, 2023

That one gets confusing, one would think f should be callable. Probably a deficiency of mixing both. We can just use this (assuming f should not be callable) and it is clear:

https://playground.solidjs.com/anonymous/cddf50e9-0050-4716-b904-a856451dcb54

(guilty pleasure: using Solid.js playground as a TS playground 😄)

@unional
Copy link
Author

unional commented Sep 23, 2023

f is not callable. const c: typeof foo is. 😆

The bottom line of these is that, the namespace "object" is the same concept of the "namespace module" as in import * as Y, which is a different construct. It is not an object nor callable.
It is just a bag to hold additional stuff (in our case here, just types).

It is a common use case thou, as in #167 (comment)

@trusktr
Copy link

trusktr commented Sep 23, 2023

I think you can just avoid exporting a type to keep it private, right?

@unional
Copy link
Author

unional commented Sep 23, 2023

I think you can just avoid exporting a type to keep it private, right?

Not really. At least not in TypeScript.

If not you will run into that nasty "the type cannot be named" error.

@lillallol
Copy link

lillallol commented Sep 23, 2023

Not really. At least not in TypeScript.

There is no local scope in .d.ts files, i.e. whatever you define in a .d.ts file, regardless of being exported or not, is available on all other .d.ts files. For .ts which export values or types, there is local scope for types (and values), i.e. what I described for .d.ts is
not valid for .ts. link

@unional
Copy link
Author

unional commented Sep 23, 2023

There is no local scope in .d.ts files

Yes. That's because .d.ts files are script files, not module files.

EDIT: I should clarify that this referring to the typical use cases.

Technically, a file is considered a script file or module file based on whether it contains top-level import or export, regardless of file extensions (as pointed out by @zaygraveyard below).

@zaygraveyard
Copy link

Further more, when a .d.ts includes an import or export statement it becomes a module file and thus nothing will be exported by default.

@unional
Copy link
Author

unional commented Sep 23, 2023

Further more, when a .d.ts includes an import or export statement it becomes a module file and thus nothing will be exported by default.

Yes. To make it clear,

script.d.ts

// this is a script file
declare type S = {}

module.d.ts

// this is a module file
export type Foo = {}

usage.ts

/// <reference path="script.d.ts" />
// ☝️ this is how to use the script file: triple-slash references

const s: S = {}

import { Foo } from './module.d.ts'
// ☝️ importing from a module d.ts file.
// It works, but generally it's better to name it as `module.ts` instead

const f: Foo

Another way to include a script file is adding them from tsconfig using files or include.

@egasimus

This comment was marked as off-topic.

@somebody1234

This comment was marked as off-topic.

@unional

This comment was marked as off-topic.

@somebody1234

This comment was marked as off-topic.

@unional

This comment was marked as off-topic.

@egasimus

This comment was marked as off-topic.

@ljharb

This comment was marked as off-topic.

@egasimus

This comment was marked as off-topic.

@azder

This comment was marked as off-topic.

@unional
Copy link
Author

unional commented Sep 27, 2023

Just sharing an example on how do I use namespace:

https://github.com/unional/type-plus/blob/main/type-plus/ts/type_plus/branch/select_with_distribute.ts

export type SelectWithDistribute<
	T,
	U,
	$O extends SelectWithDistribute.$Options = {}
> = IsAnyOrNever<
	T,
	$SelectionBranch
> extends infer R
	? R extends $Then ? $ResolveSelection<$O, T, $Else>
	: R extends $Else ? (
		$ResolveOptions<[$O['distributive'], SelectWithDistribute.$Default['distributive']]> extends true
		? SelectWithDistribute._D<T, U, $O>
		: SelectWithDistribute._N<T, U, $O>
	)
	: never : never

export namespace SelectWithDistribute {
	export type $Options = $SelectionOptions & $DistributiveOptions
	export type $Default = $SelectionPredicate & $DistributiveDefault
	export type $Branch = $SelectionBranch & $DistributiveDefault
	export type _D<T, U, $O extends SelectWithDistribute.$Options> =
		T extends U ? $ResolveSelection<$O, T, $Then> : $ResolveSelection<$O, T, $Else>
	export type _N<T, U, $O extends SelectWithDistribute.$Options> =
		[T] extends [U] ? $ResolveSelection<$O, T, $Then> : $ResolveSelection<$O, T, $Else>
}

@azder
Copy link

azder commented Sep 29, 2023

Looking at that example, maybe people should consider TS syntax isn't a goal to achieve, but a lession of what not to repeat. It's almost as verbose as SQL... How many times does one need to repeat export namespace and export type?

@unional
Copy link
Author

unional commented Oct 2, 2023

Looking at that example, maybe people should consider TS syntax isn't a goal to achieve, but a lession of what not to repeat. It's almost as verbose as SQL... How many times does one need to repeat export namespace and export type?

Generally agreed. I'm also not advocating to do it the TypeScript way.
It has a lot of technical debts making type-level programming quite hard.

But I think this is off-topic on this thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants