Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

typal

Package Overview
Dependencies
Maintainers
1
Versions
88
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

typal

Organises TypeDefs By Placing Them Into Types.Xml File To Be Embedded Into Source Code Compatible With VSCode And Google Closure Compiler, Generates Externs And Allows To Place Documentation In README Markdown.

  • 1.15.2
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
147
decreased by-18.78%
Maintainers
1
Weekly downloads
 
Created
Source

Typal

npm version

typal Keeps Types In XML files And Converts Them To (1) JavaScript JSDoc, (2) Closure Externs and (3) Markdown Documentation. It is the alternative to TypeScript definitions and utilises the power of JSDoc for excellent developer experience, documentation readability and compiler annotation. It also makes integration between Google Closure Compiler and VSCode easy, using the JSDoc notations that are understood by both at the same time.

The package's main use is as the CLI tool to generate typedefs, but it also has an API for parsing types and properties with regular expressions.

yarn add -D typal

Table Of Contents

Purpose And Use-Cases

The main purpose of this package is to generate JSDoc annotations that are understood both by VSCode, and compatible with Google Closure Compiler via its externs system. The project deliberately deviates from TypeScript and is meant for JavaScript development, and it proves that typing can be achieved perfectly well with JSDoc. It's idea is to store files in an XML file, and then embed them in JS and README files and externs.

The solutions provided by Typal are:

  1. Manage types from an external XML location.
  2. Compile types for JSDoc compatible both with GCC and VSCode.
  3. Compile types as externs for GCC and use in other packages.
  4. Place types' descriptions as formatted tables in markdown (used in Documentary).
  5. Improve the DevX by increasing the visibility of functions' APIs.

The example given below will illustrate why Typal is extremely useful as the tool both for plain JSDoc management and JSDoc for Google Closure Compiler workflow.

Naïve approach: Let's implement a transform stream that updates data using regular expressions specified in the constructor:

import { Transform } from 'stream'

export class Restream extends Transform {
  /**
   * Sets up a transform stream that updates data using the regular expression.
   * @param {Rule} rule The replacement rule.
   * @param {TransformOptions} [options] Additional options for _Transform_.
   */
  constructor(rule, options) {
    super(options)
    this.rule = rule
  }
  _transform(chunk, enc, next) {
    this.push(
      `${chunk}`.replace(this.rule.regex, this.rule.replacer)
    )
    next()
  }
}

/**
 * @typedef {Object} Rule The replacement rule.
 * @prop {RegExp} regex The regular expression.
 * @prop {(...args:string) => string} replacer The function used to update input.
 * @typedef {import('stream').TransformOptions} TransformOptions
 */

In the file, we have defined a type using typedef, and imported a type from the internal Node.JS API. All is well, and we get our JSDoc autosuggestions that help us understand that what we're doing is correct.

However, there are 2 problems with that:

  1. Google Closure Compiler does not understand typedefs a) without variable declaration underneath, b) with @properties and c) with functions in (...args: string) => string notation. The format for GCC typedef for our example would be the one below. And if we tried to use it, VSCode would not understand it, and we would loose the description of individual properties of the type.
    /**
     * @typedef {{ regex: RegExp, replacement: function(...string): string }}
     */
    var Rule
    
  2. Google Closure Compiler does not understand @typedef {import('from').Name} syntax. It is currently not supported, and to be able to reference types from other packages, they must have externs. So for the TransformOptions, we need stream.TransformOptions externs. To reference types from the same package but across files, GCC will need types to be imported as ES6 imports (like how things were in 2018), e.g.,
    import Rule from './src' // pre-typedef import
    /**
     * @param {Rule} rule
     */
    const fn = (rule) => {}
    
  3. The documentation that we wrote as JSDoc type declarations has to be copied and pasted into the README.md file manually, and all tables need to be also constructed.
  4. It is not clear what interface the Rule type adheres to, because VSCode does not show that information:

JSDoc approach: Now let's refactor the code that we have, and place the types definitions in the types.xml file instead of the source code:

<types>
  <import from="stream" name="TransformOptions"
    link="https://nodejs.org/api/stream.html#stream_class_stream_transform" />
  <type name="Rule" desc="The replacement rule." noToc>
    <prop type="RegExp" name="regex">
      The regular expression.
    </prop>
    <prop type="(...args:string) => string" name="replacement">
      The function used to update input.
    </prop>
  </type>
</types>

The types files support <import>, <type> and <prop> tags. We then update the source code to indicate the location of where types should be read from (there needs to be a newline before the end of the file):

import { Transform } from 'stream'

export class Restream extends Transform {
  /**
   * Sets up a transform stream that updates data using the regular expression.
   * @param {Rule} rule The replacement rule.
   * @param {TransformOptions} [options] Additional options for _Transform_.
   */
  constructor(rule, options) {
    super(options)
    this.rule = rule
  }
  // ...
}

/* typal example/restream/types.xml */

Then, we call the typal binary to get it to update the source: typal example/restream/index.js:

import { Transform } from 'stream'

export class Restream extends Transform {
  /**
   * Sets up a transform stream that updates data using the regular expression.
   * @param {Rule} rule The replacement rule.
   * @param {RegExp} rule.regex The regular expression.
   * @param {(...args:string) => string} rule.replacement The function used to update input.
   * @param {TransformOptions} [options] Additional options for _Transform_.
   */
  constructor(rule, options) {
    super(options)
    this.rule = rule
  }
  // ...
}

/* typal example/restream/types.xml */
/**
 * @typedef {import('stream').TransformOptions} TransformOptions
 * @typedef {Object} Rule The replacement rule.
 * @prop {RegExp} regex The regular expression.
 * @prop {(...args:string) => string} replacement The function used to update input.
 */

From that point onward, the JSDoc documentation is managed from the separate file. It can also be embedded into the Markdown, using the Documentary documentation pre-processor by adding the %TYPEDEF: example/restream/types.xml% marker in the README file:

import('stream').TransformOptions stream.TransformOptions

Rule: The replacement rule.

NameTypeDescription
regex*RegExpThe regular expression.
replacement*(...args:string) => stringThe function used to update input.

The link to the Rule type was also added to the Table of Contents, however it can be skipped if the type element had the noToc property set on it. We also added the link property to the import element to place a link to Node.JS API docs in documentation.

Another advantage, is that the Rule type was expanded into individual properties in JSDoc above the constructor method. It allows to preview all properties and their descriptions when hovering over functions:

Closure approach: Finally, if we want to allow our package to be compiled as part of other packages with GCC (or compile a binary from the lib we've written), we need to make sure the JSDoc is in the format that it accepts.

We create a simple program that uses our Restream library:And run it with Node.JS:
import { Restream } from 'restream'

const restream = new Restream({
  regex: /__(.+?)__/,
  replacement(match, s) {
    return `<em>${s}</em>`
  },
})

restream.pipe(process.stdout)
restream.end('__hello world__')
<em>hello world</em>

Let's try to compile a program using GCC now (using Depack) and see what happens:

Shell Command To Spawn Closure
java -jar google-closure-compiler-java/compiler.jar --compilation_level ADVANCED \
--language_out ECMASCRIPT_2017 --formatting PRETTY_PRINT \
--externs @depack/externs/v8/stream.js --externs @depack/externs/v8/events.js \
--externs @depack/externs/v8/global.js --externs @depack/externs/v8/nodejs.js \
--module_resolution NODE --output_wrapper "#!/usr/bin/env node
'use strict';
const stream = require('stream');%output%" \
--js node_modules/stream/package.json \
     node_modules/stream/index.js \
     example/restream/program.js \
     example/restream/compat.js

The command above was generated with Depack call on the right, where:

  • -c means Node.JS compilation (adds the wrapper, mocks and externs),
  • -a means ADVANCED mode,
  • and -p means pretty-printing.
depack example/restream/program -c -a -p

Google Closure Compiler does not discover source code files the list of which must be passed manually. In addition, it does not work with internal Node.JS modules natively. The software that performs static analysis of programs to detect what files to feed to the compiler, as well as mocking Node.JS built-in modules in the node_modules folder and providing externs for them is called Depack.

After finishing its job, the compiler will give us warnings shown below, which tell us that the program was not type-checked correctly. Sometimes we can ignore warnings, but we loose the ability to ensure correct typing. It is also possible that the compiler will perform the advanced optimisations incorrectly by mangling property names (e.g., regex becomes a), but it is not the case here because all files are used together, but if we were publishing the library, the first parameter rule would not adhere to the Rule interface.

Google Closure Compiler Warnings
restream/index2.js:6: WARNING - Bad type annotation. Unknown type Rule
   * @param {Rule} rule The replacement rule.
             ^

restream/index2.js:8: WARNING - Bad type annotation. type not recognized due to syntax error.
See https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler
for more information.
   * @param {(...args:string) => string} rule.replacement The function used to update input.
              ^

restream/index2.js:9: WARNING - Bad type annotation. Unknown type TransformOptions
   * @param {TransformOptions} [options] Additional options for _Transform_.
             ^

restream/index2.js:25: WARNING - Bad type annotation. expected closing }
See https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler
for more information.
 * @typedef {import('stream').TransformOptions} TransformOptions
                   ^

restream/index2.js:26: WARNING - Bad type annotation. type annotation incompatible with
other annotations.
See https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler
for more information.
 * @typedef {Object} Rule The replacement rule.
   ^

The warnings produced by the compiler tell us the points discussed in the beginning:

  • the classic typedefs {Object} Rule,
  • function types (...args:string) => string,
  • and imports import('stream').TransformOptions are not understood.

This is because the traditional JSDoc annotation is not compatible with the compiler. To solve that, we need to compile JSDoc in Closure mode with Typal. First, we want to adjust our types to include more features:

Updated Types For Closure (view source)
<types namespace="_restream">
  <import from="stream" name="TransformOptions"
    link="https://nodejs.org/api/stream.html#stream_class_stream_transform" />
  <type name="Rule" desc="The replacement rule.">
    <prop type="!RegExp" name="regex">
      The regular expression.
    </prop>
    <prop type="(...args:string) => string"
      closure="function(...string): string" name="replacement">
      The function used to update input.
    </prop>
  </type>
  <type type="!Array<!_restream.Rule>" name="Rules"
    desc="Multiple replacement rules.">
  </type>
</types>
  1. Annotate the nullability of our types using !, since there's attention to null in GCC, not like traditional JS.
  2. We also add the closure property to the prop elements to make them use that type instead of the traditional one. Unfortunately, there's no way to use both in code for VSCode and for GCC, however we can still use more readable type descriptions when generating README documentation.
  3. Add the namespace, because we're going to generate externs and if there are other programs that define the Rule extern, there would be a conflict between the two. Adding namespace ensures that the chances of that happening are minimal. In addition, we prefix the namespace with _ because we'll put it in externs, and if we or people using our library called a variable restream, the compiler will think that its related to the extern which it is not because it's a namespace in externs, but an instance of Restream in source code.
  4. Finally, add another type Rules just to illustrate how to reference types across and within namespaces. Although defined in the same namespace, the properties need to give full reference to the type.

If we now compile the source code using the --closure flag (so that the command is typal example/restream/closure.js -c), our source code will have JSDoc that is fully compatible with the Google Closure Compiler:

The Source Code With Closure-Compatible JSDoc (view source)
import { Transform } from 'stream'

export class Restream extends Transform {
  /**
   * Sets up a transform stream that updates data using the regular expression.
   * @param {!_restream.Rule} rule The replacement rule.
   * @param {!RegExp} rule.regex The regular expression.
   * @param {(...args:string) => string} rule.replacement The function used to update input.
   * @param {!stream.TransformOptions} [options] Additional options for _Transform_.
   */
  constructor(rule, options) {
    super(options)
    this.rule = rule
  }
  _transform(chunk, enc, next) {
    this.push(
      `${chunk}`.replace(this.rule.regex, this.rule.replacement)
    )
    next()
  }
}

There have to be some manual modifications to the source:

  • We rename the @params to use the namespace and make it non-nullable since it's a thing in Closure, i.e., if we don't do it the type of the param will actually be (restream.Rule|null): @param {_!restream.Rule} rule;
  • We also add the namespace to the internal module @param {!stream.TransformOptions}, because in Closure the externs are provided for the stream namespace.

The following changes are introduced automatically by Typal after we started using the --closure mode:

/* typal example/restream/types2.xml */
/**
  * @suppress {nonStandardJsDocs}
  * @typedef {_restream.Rule} Rule The replacement rule.
  */
/**
  * @suppress {nonStandardJsDocs}
  * @typedef {Object} _restream.Rule The replacement rule.
  * @prop {!RegExp} regex The regular expression.
  * @prop {function(...string): string} replacement The function used to update input.
  */
The Rule type is now defined using 2 @typedefs, which are also suppressed to prevent warnings. The reason for the first item is so that the type can be imported in other files from our package, using {import('restream').Rule}. This is so because {import('restream')._restream.Rule} does not work in VSCode. The second type stays as is, and is printed with the namespace. It is still not picked up by GCC, but the warning is suppressed. Instead, when we come to generate externs in a minute, their name will match _restream.Rule, and the param for the function will be recognised by the compiler.
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {import('stream').TransformOptions} stream.TransformOptions
 */
The imports are now also suppressed (but the change will hopefully come into effect in the next version of the compiler), and printed with the namespace, so that we can refer to them in params and get both the autosuggestions, and Closure compatibility.
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {_restream.Rules} Rules Multiple replacement rules.
 */
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {!Array<!_restream.Rule>} _restream.Rules Multiple replacement rules.
 */
Any types within the namespace must refer to each other using their full name.

Before we continue to compilation, we still need to generate externs, because the Closure compiler does not know about the Rule type. Externs is the way of introducing types to the compiler, so that it can do type checking and property renaming more accurately. Once again, we place the /* typal example/restream/types2.xml */ marker in the empty externs.js file, and let Typal to the job with typal example/restream/externs.js --externs command (or -e).

Generated Externs For Restream (view source)
/* typal example/restream/types2.xml */
/** @const */
var _restream = {}
/**
 * @typedef {{ regex: !RegExp, replacement: function(...string): string }}
 */
_restream.Rule
/**
 * @typedef {!Array<!_restream.Rule>}
 */
_restream.Rules
The externs are generated with the Closure-compatible syntax and ready to be used for compilation of our example program.

To continue, we run depack example/restream/program -c -a -p --externs restream/externs.js again, and this time, Depack will pass the externs argument to the compiler as we request.

Result Of Compilation
#!/usr/bin/env node
'use strict';
const stream = require('stream');             
const {Transform:c} = stream;
class d extends c {
  constructor(a, b) {
    super(b);
    this.a = a;
  }
  _transform(a, b, f) {
    this.push(`${a}`.replace(this.a.regex, this.a.replacement));
    f();
  }
}
;const e = new d({regex:/__(.+?)__/, replacement(a, b) {
  return `<em>${b}</em>`;
}});
e.pipe(process.stdout);
e.end("__hello world__");
stdout
java -jar /Users/zavr/node_modules/google-closure-compiler-java/compiler.jar \
--compilation_level ADVANCED --language_out ECMASCRIPT_2017 --formatting PRETTY_PRINT \
--externs example/restream/externs.js --package_json_entry_names module,main \
--entry_point example/restream/program.js --externs ../../depack/externs/v8/stream.js \
--externs ../../depack/externs/v8/events.js --externs ../../depack/externs/v8/global.js \
--externs ../../depack/externs/v8/global/buffer.js --externs \
../../depack/externs/v8/nodejs.js
Modules: example/restream/compat.js
Built-ins: stream
Running Google Closure Compiler 20190709.           
stderr

Although we've generated the externs and passed them to the compiler, we don't actually need them here when generating a single executable file. Notice how the compiler didn't rename the regex and replacement properties of the rule variable, but the variable itself is stored inside of the class as a. This is precisely the point of externs — to prevent the compiler from mangling properties that can come from outside code. Now, if we were compiling a library for use by other developers, and publishing it, we would want to prevent mangling optimisation, and then we would use externs. However, this optimisation only happens in the ADVANCED mode, where all comments with JSDoc is stripped, making the library hard-to use by others. But when we create a program and not a library, we can avoid using the externs, and pass the types just as a source file using the --js flag. This will still result in type-checking but also produce the optimisation of variable names (though in case of Node.JS programs the gain is minimal because the difference in size is not that significant, but for the web it might be helpful).

Externs As Types
#!/usr/bin/env node
'use strict';
const stream = require('stream');             
const {Transform:c} = stream;
class d extends c {
  constructor(a, b) {
    super(b);
    this.a = a;
  }
  _transform(a, b, f) {
    this.push(`${a}`.replace(this.a.b, this.a.c));
    f();
  }
}
;const e = new d({b:/__(.+?)__/, c(a, b) {
  return `<em>${b}</em>`;
}});
e.pipe(process.stdout);
e.end("__hello world__");
The new command is depack example/restream/program -c -a -p --js example/restream/externs.js and it produces correctly optimised code.

And so that's it! We've successfully compiled our Node.JS program with Google Closure Compiler using Depack as the CLI interface, and Typal as the utility to organise types, both for README documentation, JSDoc annotation and Compiler externs information. There is just one last thing to add.

Annotating Types
import { Restream } from 'restream'

/**
 * The rule to enable `<em>` tag conversion from Markdown.
 * @type {_restream.Rule}
 */
const rule = {
  regex: /__(.+?)__/,
  replacement(match, s) {
    return `<em>${s}</em>`
  },
}
const restream = new Restream(rule)

restream.pipe(process.stdout)
restream.end('__hello world__')

/**
 * @suppress {nonStandardJsDocs}
 * @typedef {import('restream').Rule} _restream.Rule
 */

When writing code that imports types from libraries, we can use the {import('lib').Type} notation for VSCode to give us auto-completions, but we need to suppress it. However, because now we're naming imported types with the namespace, Closure will pick them up from externs if it finds it. Packages can publish their externs and point to them using the externs field in their package.json file, which will be read by Depack and passed to GCC in the --externs flag.

CLI

Typal is the command-line utility that is used to manage JSDoc types in JavaScript source files. The typedefs are now sourced from the types.xml file and embedded on demand. There are 3 modes to embedding types:

  1. Standard, no flags required: places only VSCode compatible code. Can be used when no Closure-compilation will be performed on packages. Does not utilise namespaces. Expands the parameters of complex types for better visibility.

    Show Standard JSDoc
    /**
     * @param {Conf} conf The configuration object.
     * @param {string} conf.source The source of where to read the data.
     * @param {boolean} [conf.closeOnFinish=true] Closes the stream when done. Default `true`.
     * @param {TransformOptions} options
     */
    const prog = (conf, options) => {}
    
    /* typal example/cli/types.xml */
    /**
     * @typedef {import('stream').TransformOptions} TransformOptions
     * @typedef {Object} Conf The configuration object.
     * @prop {string} source The source of where to read the data.
     * @prop {boolean} [closeOnFinish=true] Closes the stream when done. Default `true`.
     */
    
  2. Closure with -c flag: suppresses standard typedefs' annotations so that Closure Compiler does not show warnings. Introduces namespaces for internal as well as external APIs to make types' sources more visible.

    Show Closure JSDoc
    /**
     * @param {_typal.Conf} conf The configuration object.
     * @param {string} conf.source The source of where to read the data.
     * @param {boolean} [conf.closeOnFinish=true] Closes the stream when done. Default `true`.
     * @param {stream.TransformOptions} options
     */
    const prog = (conf, options) => {}
    
    /* typal example/cli/types.xml */
    /**
     * @suppress {nonStandardJsDocs}
     * @typedef {_typal.Conf} Conf The configuration object.
     */
    /**
     * @suppress {nonStandardJsDocs}
     * @typedef {Object} _typal.Conf The configuration object.
     * @prop {string} source The source of where to read the data.
     * @prop {boolean} [closeOnFinish=true] Closes the stream when done. Default `true`.
     */
    /**
     * @suppress {nonStandardJsDocs}
     * @typedef {import('stream').TransformOptions} stream.TransformOptions
     */
    
  3. Externs with -e flag: generates types only understood by the Google Closure Compiler, primarily in the externs.js file. These types do not have any meaning for the coding process and are only used in compilation either as types for programs, or externs for libraries.

    Show Externs JSDoc
    /* typal example/cli/types.xml */
    /** @const */
    var _typal = {}
    /**
     * The configuration object.
     * @typedef {{ source: string, closeOnFinish: (boolean|undefined) }}
     */
    _typal.Conf
    

Typal Arguments

$ typal source [--closure|externs] [-o output] [-vh]

The following arguments are supported by this software.

ArgumentShortDescription
source The path to the source file or directory with files to embed types into. Can specify multiple values, e.g., typal types/index.js types/vendor.js.
--output-o The destination where to save output. If not passed, the file will be overwritten. If - is passed, prints to stdout.
--closure-c Whether to generate types in Closure mode.
--externs-e Whether to generate externs for GCC.
--types-t Comma-separated location of files to read types from.
--migrate-m Extracts types from JavaScript source code and saves them into the types.xml file specified in the output option.
--help-hPrint the help information and exit.
--version-vShow the version's number and exit.

Typal will look for its marker in the source files, and insert all types definitions below it. There must be a single new line after the marker, even at the bottom of the file. It is possible to override the arguments, or pass them via the marker itself. When these are specified, there is no need to supply them via the CLI.

function sourceCode() {}

/* typal types/index.xml [closure|externs] [skipNsDecl] [noSuppress] [ignore:_ns.Type,Type] */
_ // remember new line!
  • closure: enable the closure mode;
  • externs: enable the externs mode;
  • noSuppress: don't add @suppress annotations (see the files section below).
  • ignore:_nsType,Type: the types to ignore when placing JSDoc into JS files. This can be useful, for example, when the package is built with Depack and has no dependencies, but depends on imported types from other packages. Therefore, these imported types need to be vendored using a separate file, and then imported from there, rather than from their original source file. See @zoroaster/mask/types/vendor.js and @zoroaster/mask/types/index.js for a practical application.
  • skipNsDecl: Disables the declaration of the namespace. The types will still be prefixed with a namespace, but it won't be declared at the top as /** @const */ var ns = {}. This is useful when the externs are split by multiple files, and the namespace will only need to appear in one of them, otherwise the Variable _ns declared more than once. error will be thrown.

Missing Types Warnings

When placing JSDoc into source code files where functions are annotated with @params, Typal in addition to expanding object arguments into the developer-friendly notation as discussed above, will check to see if the types were found in the xml files specified in via the /* typal types.xml */ marker to warn of possible errors. This feature aims at helping to identify when some annotations were not done properly, e.g., when missing a namespace, an import, or when type names become outdated. This does not work for record types such as {} since although we have a parser for types themselves, we only use a regular expression which cannot understand things like @param {{ s: string, t }} at the moment. Also only Closure-style types are parsed, i.e., VSCode JSDoc is not supported right now, and the union must be explicitly put in parenthesis.

/**
 * @param {stream.Writable} writable
 * @param {stream.Readable} readable
 * @param {_ns.Type} type
 * @param {_ns.MissingType} missingType
 * @param {Array<_ns.MissingType>} array
 * @param {Promise<MissingType>} promise
 * @param {Object<string, _ns.MissingType>} object
 * @param {(Type | MissingType | _ns.Type)} union
 * @param {(s: string) => number} error
 * @param {MissingType & Type2} intersection Only first type will be parsed
 * @param {string} string
 */
function example (
  writable, readable,
  type, missingType,
  array, promise, object, union,
  error,
  string,
) {}

/* typal example/warnings.xml */
Detected type marker: example/warnings.xml
Type stream.Readable was not found.
example/warnings.js:3:11
Type _ns.MissingType was not found.
example/warnings.js:5:11
Type _ns.MissingType in Array<_ns.MissingType> was not found.
example/warnings.js:6:11
Type MissingType in Promise<MissingType> was not found.
example/warnings.js:7:11
Type _ns.MissingType in Object<string, _ns.MissingType> was not found.
example/warnings.js:8:11
Type Type in (Type | MissingType | _ns.Type) was not found.
example/warnings.js:9:11
Type MissingType in (Type | MissingType | _ns.Type) was not found.
example/warnings.js:9:11
Error while parsing the type (s: string) => number
Expecting closing )
example/warnings.js:10:11
Type MissingType in MissingType & Type2 was not found.
example/warnings.js:11:11

Keeping Types In Separate File

If the types are numerous and it is desired to put them in a separate JS file (like types.d.ts but for JSDoc) and then import them in code from there for expansions of function's configs, it is possible with the -t argument pointing to the location of XML files. Keeping all files in a types.js file allows to import them from anywhere in the code, or other packages (the file needs to be added to the files field of package.json, if such field exists).

For example, we can create a types.js file with the typal marker:

// types.js
export {} // important for enabling of importing
/* typal types/index.xml closure noSuppress */

The types can be placed in there with typal types.js command. We also add the noSuppress command because the file will not be imported and checked by the Google Closure Compiler therefore the @suppress annotations would be redundant. Now the aim is to update the source code which has a variable of a particular type that we want to expand and we run typal src/index.js -t types/index.xml to do that:

// src/index.js
/**
 * @param {_ns.Config} config
 */
function example(config = {}) {
  const { test } = config
}

// manually add the namespace and dependencies' imports
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {import('stream').Readable} stream.Readable
 */
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {import('../types').Config} _ns.Config
 */
// src/index.js
/**
 * @param {_ns.Config} config The config for the program
 * @param {string} config.test The test property.
 * @param {stream.Readable} config.rs The stream to read.
 */
function example(config = {}) {
  const { test } = config
}

// manually add the namespace and dependencies' imports
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {import('stream').Readable} stream.Readable
 */
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {import('../types').Config} _ns.Config
 */

Any external types referenced in properties must be manually imported, because otherwise their types will be unknown in the scope of the file. This can be done with the snippet that can be put either in the workspace directory as .vscode/import.code-snippets, or configured to be included in User Snippets (⇧ P > Preferences: Configure User Snippets).

{
	"Import Type And Suppress": {
		"prefix": "@typedef",
		"body": [
			"/**",
			" * @suppress {nonStandardJsDocs}",
			" * @typedef {import('$1')$2} $3",
			" */"
		],
		"description": "Insert import typedef"
	}
}

In future, we plan to introduce full-scale management of types so that all import statements will be added automatically by Typal.

Migration

When there are JSDoc types written in JavaScript files, and they need to be put in the types.xml file, it can be done automatically with the --migrate command. In this case, Typal will scan the source code for the type definitions and their properties, defined as @prop or @property tags, and place them either in the output file when specified, or print to the stdout. This will help to move all types into XML declarations, which can then be manually adjusted if necessary, and embedded into the source code using the /* typal types.xml */ marker, and in README documentation using Documentary.

Using Migrate Command
/**
 * @typedef {import('koa-multer').StorageEngine} StorageEngine
 * @typedef {import('http').IncomingMessage} IncomingMessage
 * @typedef {import('koa-multer').File} File
 */

/**
 * @typedef {Object} Example An example type.
 * @typedef {Object} SessionConfig Description of Session Config.
 * @prop {string} key The cookie key.
 * @prop {number|'session'} [maxAge=86400000] maxAge in ms. Default is 1 day.
 * @prop {boolean} [overwrite] Can overwrite or not. Default `true`.
 * @prop {boolean} [httpOnly] httpOnly or not or not. Default `true`.
 * @prop {boolean} [signed=false] Signed or not. Default `false`.
 * @prop {boolean} [rolling] Force a session identifier cookie to be set.
 * @prop {boolean} [renew] Renew session when session is nearly expired.
 */
For example, the types above can be extracted into the types file using the typal src/index.js -m [-o types/index.xml] command.
<types>
  <import name="StorageEngine" from="koa-multer" />
  <import name="IncomingMessage" from="http" />
  <import name="File" from="koa-multer" />
  <type name="Example" desc="An example type." />
  <type name="SessionConfig" desc="Description of Session Config.">
    <prop string name="key">
      The cookie key.
    </prop>
    <prop type="number|'session'" name="maxAge" default="86400000">
      maxAge in ms. Default is 1 day.
    </prop>
    <prop boolean name="overwrite" default="true">
      Can overwrite or not.
    </prop>
    <prop boolean name="httpOnly" default="true">
      httpOnly or not or not.
    </prop>
    <prop boolean name="signed" default="false">
      Signed or not.
    </prop>
    <prop opt boolean name="rolling">
      Force a session identifier cookie to be set.
    </prop>
    <prop opt boolean name="renew">
      Renew session when session is nearly expired.
    </prop>
  </type>
</types>

Schema

The types can be defined according to the following schema. It consists of the types, type and property elements.

Types

<types
  namespace="_namespace">
  <import .../>
  <type ...>...</type>
</types>

The single root element for the XML file.

  • namespace [optional]: how all types will be prefixed in the source code and externs. The use of namespaces is generally only needed for when using GCC to prevent clashes of types, e.g., it is common to name the config objects "Config". The namespace will typically start with _ to also prevent variable name clashes with extern namespaces.
    // SOURCE.js
    // The first line is to enable exporting via VSCode's typedef import.
    /**
     * @typedef {_restream.Rule} Rule The replacement rule.
     */
    // The second line is to use within the source file, so that the externs
    // match the annotated type.
    /**
     * @typedef {Object} _namespace.Rule The replacement rule.
     */
    
    /**
     * @param {_namespace.Rule} rule
     */
    function hello(rule) {}
    
    // EXTERNS.js
    /** @const */
    var _namespace = {}
    /** @typedef { myType: boolean } */
    _namespace.Type
    

Type

The type represents a JSDoc type.

<type
  name="Type"
  desc="The description of the type."
  type="(name: string) => number"
  constructor interface record
  extends="_namespace.ParentType"
  closure="function(string): number">
    <prop name="...">...</prop>
</type>
  • name: [required]: the name of the type.

  • desc [optional]: the optional description.

  • type [optional]: what is the type, default Object.

  • constructor [optional]: for externs, adds the @constructor annotation and declares the properties via the prototype.

    Show Prototype Notation
    /* typal example/schema/constructor.xml */
    /** @const */
    var _test = {}
    /**
     * The example type.
     * @extends {_ns.ParentType}
     * @constructor
     */
    _test.Test
    /**
     * A prop.
     * @type {boolean|undefined}
     */
    _test.Test.prototype.bool
    
  • interface [optional]: for externs, same as @constructor, but adds the @interface annotation.

  • record [optional]: for externs, same as @constructor, but adds the @record annotation. This type is called Structural Interfaces and is the best choice for configs etc. Types without @constructor/@interface/@record in externs will be presented as {{ a: string, b: number }} but when denoted with @record, externs will have the same meaning, but will be easier to read. However, @record types can be nullable, whereas simple {{ record }} types are explicitly non-nullable.

  • extends [optional]: for constructors, interfaces and records this allows to inherit properties from the parent types (see above).

    Show Extends Notation
    Extends Type (view extends.xml)
    /* typal example/schema/extends.xml */
    /**
     * @suppress {nonStandardJsDocs}
     * @typedef {_test.Test} Test `@record` The example type.
     */
    /**
     * @suppress {nonStandardJsDocs}
     * @typedef {_ns.ParentType & _test.$Test} _test.Test `@record` The example type.
     */
    /**
     * @suppress {nonStandardJsDocs}
     * @typedef {Object} _test.$Test `@record` The example type.
     * @prop {boolean} [bool] A prop.
     */
    
    JSDoc typedefs will contain an extra class denoted with $ to be able to extend the parent class, because there's no other way to do it: if the typedef had the parent in its type notation (instead of {Object}), then the properties wouldn't be applied. The internal $ class is then merged with the parent class using the & symbol which is TypeScript-specific, but understood by VSCode (not part of the JSDoc spec, but should be).
    /* typal example/schema/extends.xml */
    /** @const */
    var _test = {}
    /**
     * The example type.
     * @extends {_ns.ParentType}
     * @record
     */
    _test.Test
    /**
     * A prop.
     * @type {boolean|undefined}
     */
    _test.Test.prototype.bool
    
    Externs just add the @extends marker when the type is either @constructor, @interface or @record.
  • closure [optional]: an override of the type when generating doc in closure mode.

    Show Closure Override
    Closure Override (view closure.xml)
    /* typal example/schema/closure.xml */
    /**
     * @suppress {nonStandardJsDocs}
     * @typedef {_test.Example} Example
     */
    /**
     * @suppress {nonStandardJsDocs}
     * @typedef {function(string): number} _test.Example
     */
    
    In Closure mode, Typal will print the value of the closure property. This is helpful for displaying user-readable documentation in README files, but using the types for compilation. There's no way to use both in source code (i.e., the standard type for VSCode and the closure type for GCC).
    /* typal example/schema/closure.xml */
    /**
     * @typedef {(s: string) => number} Example
     */
    
    In standard mode, only the type attribute is displayed. This is not compatible with GCC, therefore should only be used for JSDoc approach programming.

Property

The properties are found inside of the Type elements. At the moment, the must have a description, otherwise the parsing won't work.

<prop
  name="property"
  string boolean number type="Type"
  opt default="The default value"
  closure="_ns.Type">
Property Description.
</prop>
  • name: the name of the property.
  • string [optional]: sets the type to be string.
  • boolean [optional]: sets the type to be boolean.
  • number [optional]: sets the type to be number.
  • type [optional]: sets the type of the property. Default *.
  • opt [optional]: whether the property is optional. In externs this will result in { prop: (string|undefined) }.
  • default [optional]: the default value of the property. Used to add the Default: value. to the property description, and @param {type} [prop=default] when annotating JS functions.
  • closure [optional]: an override of the type when generating doc in closure mode.
Properties (view property.xml)
<types>
  <type name="Example">
    <prop boolean name="boolean">The boolean property.</prop>
    <prop number name="number">The number property.</prop>
    <prop string name="string">The string property.</prop>
    <prop type="Example" name="example">The custom type property.</prop>
    <prop opt type="Example" name="optional">The optional property.</prop>
    <prop string name="default" default="hello-world">
      The default property.
    </prop>
    <prop type="Example" closure="_ns.Example" name="closure">
      The Closure override property.
    </prop>
  </type>
</types>
The properties are listed inside of types and must have descriptions which are trimmed.
/* typal example/schema/property.xml */
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {Object} Example
 * @prop {boolean} boolean The boolean property.
 * @prop {number} number The number property.
 * @prop {string} string The string property.
 * @prop {Example} example The custom type property.
 * @prop {Example} [optional] The optional property.
 * @prop {string} [default="hello-world"] The default property. Default `hello-world`.
 * @prop {_ns.Example} closure The Closure override property.
 */
Typal will extract properties from xml file and insert them into JSDoc.

Import

<import
  name="Type"
  from="package-name/src"
  ns="_packageName"
  link="https://docs.page/package-name"
  desc="The imported type from another package.">
</import>
  • name: the name of the imported type.
  • from: the package (restream) or path (restream/src/Rule) to import from.
  • ns [optional]: if different from the path, the namespace with which the type will be imported.
  • link [optional]: the link to display in documentation with Documentary.
  • desc [optional]: the description to print in documentation.
Imports (view import.xml)
/* typal example/schema/import.xml */
/**
 * @typedef {import('restream').Rule} Rule
 * @typedef {import('restream/src/markers').Marker} Marker
 * @typedef {import('stream').Readable} Readable
 */
In standard mode, Typal does not use namespaces.
/* typal example/schema/import.xml */
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {import('restream').Rule} _restream.Rule
 */
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {import('restream/src/markers').Marker} _restream.Marker
 */
/**
 * @suppress {nonStandardJsDocs}
 * @typedef {import('stream').Readable} stream.Readable
 */
In Closure mode, Typal adds namespaces so that they will match externs.

Markdown Documentation

Typal allows to paste types into documentation using the Documentary package. It will also link the types it knows about for easier navigation. The supported types are based on the Google Closure Compiler types and include the following:

Type: A type which can be linked.

Example: An example type which can link to other types.

NameTypeDescription
type?TypeThe type itself, possibly nullable.
union!(Type | string)The union of types.
record{ t: Type, r }The record with a type.
applicationObject<string, Type>The application with a type.
functionfunction(this: Type, string, !Type): TypeThe function with arguments and return type.
variable-argsfunction(...Type)Functions with ... for variable argument types.
vscode-function(type: Type, s: string) => TypeLinking in the VSCode (TypeScript) functions are not supported at the moment.

API

The package is available by importing its named functions and classes:

import { Type, Property, getNameWithDefault, parseFile } from 'typal'

Its primary use is in Documentary, and the API is therefore semi-private.

class Type

This class represents the type.

class Property

This class represents the properties of the type.

getNameWithDefault(
  name: string,
  defaultValue: ?(string|boolean|number),
  type: string=,
  parentParam: string=,
): string

Returns the name of a property with its default value, and surrounded by square brackets if default is given. If type is boolean or number, the default value is not surrounded by "". The default values are only used for VSCode because GCC does not use this information.

/**
 * @param {*} requiredParam
 * @param {*} [optionalDefaultParam=false]
 * @param {*} [optionalDefaultParamString="test"]
 * @param {*} [optionalParam]
 *
 * @param {*} parentParam.requiredParam
 * @param {*} [parentParam.optionalDefaultParam=false]
 * @param {*} [parentParam.optionalDefaultParamString="test"]
 * @param {*} [parentParam.optionalParam]
 */
import { getNameWithDefault } from 'typal'

console.log(getNameWithDefault('arg', 'test', 'string'))
console.log(getNameWithDefault('hello', true, 'boolean', 'arg'))
console.log(getNameWithDefault('world', 27, 'number', 'arg'))
arg="test"
arg.hello=true
arg.world=27

parseFile(
  xml: string,
  rootNamespace: string=,
): { types, imports, namespace }

Returns the string parsed into Types and Properties.

Given the following types file:

<types>
  <import name="ServerResponse" from="http" />
  <type name="SetHeaders"
    type="(s: ServerResponse) => void"
    closure="function(http.ServerResponse)"
    desc="Function to set custom headers on response." />
  <type name="StaticConfig" desc="Options to setup `koa-static`.">
    <prop string name="root">
      Root directory string.
    </prop>
    <prop number name="maxage" default="0">
      Browser cache max-age in milliseconds.
    </prop>
    <prop boolean name="hidden" default="false">
      Allow transfer of hidden files.
    </prop>
  </type>
</types>

It can be parsed using the following call:

import read from '@wrote/read'
import { parseFile } from 'typal'

const getFile = async () => {
  const file = await read('test/fixture/types.xml')
  const res = parseFile(file)
  return res
}

The result will contain Types and Imports:

{ namespace: undefined,
  types: 
   [ Type {
       name: 'SetHeaders',
       type: '(s: ServerResponse) => void',
       closureType: 'function(http.ServerResponse)',
       description: 'Function to set custom headers on response.',
       noToc: false,
       spread: false,
       import: false,
       noExpand: false,
       link: null,
       properties: [],
       namespace: null,
       isConstructor: false,
       isInterface: false,
       isRecord: false,
       extends: null },
     Type {
       name: 'StaticConfig',
       type: null,
       closureType: null,
       description: 'Options to setup `koa-static`.',
       noToc: false,
       spread: false,
       import: false,
       noExpand: false,
       link: null,
       properties: 
        [ Property {
            name: 'root',
            description: 'Root directory string.',
            type: 'string',
            closureType: 'string',
            hasDefault: false,
            default: null,
            optional: false },
          Property {
            name: 'maxage',
            description: 'Browser cache max-age in milliseconds.',
            type: 'number',
            closureType: 'number',
            hasDefault: true,
            default: 0,
            optional: true },
          Property {
            name: 'hidden',
            description: 'Allow transfer of hidden files.',
            type: 'boolean',
            closureType: 'boolean',
            hasDefault: true,
            default: false,
            optional: true } ],
       namespace: null,
       isConstructor: false,
       isInterface: false,
       isRecord: false,
       extends: null } ],
  imports: 
   [ Import {
       ns: 'http',
       name: 'ServerResponse',
       from: 'http',
       desc: undefined,
       link: undefined } ],
  Imports: 
   [ Type {
       name: 'ServerResponse',
       type: 'import(\'http\').ServerResponse',
       closureType: 'import(\'http\').ServerResponse',
       description: null,
       noToc: true,
       spread: false,
       import: true,
       noExpand: false,
       link: null,
       properties: [],
       namespace: 'http',
       isConstructor: false,
       isInterface: false,
       isRecord: false,
       extends: null } ] }
Root Namespace

Passing the rootNamespace allows to ignore the given namespace in types and properties. This can be used for compiling documentation when only single namespace is used, and readers can assume where the types come from. However, this should only be used when printing to docs, but when compiling JSDoc, the full namespaces should be used to allow integration with externs.

Given the following types file which uses namespaces:

<types namespace="ns">
  <type name="HelloWorld" desc="The example type.">
  </type>
  <type type="ns.HelloWorld" name="GoodMorning"
    desc="Life is seeing sunlight every day." />
  </type>
  <type name="Conf" desc="The configuration object">
    <prop type="ns.HelloWorld" name="propName">
      The property description.
    </prop>
  </type>
</types>

It can be parsed so that the ns. prefix is ignored:

import read from '@wrote/read'
import { parseFile } from 'typal'

const getFile = async () => {
  const file = await read('example/root.xml')
  const res = parseFile(file, 'ns')
  return res
}
{ namespace: 'ns',
  types: 
   [ Type {
       name: 'HelloWorld',
       type: null,
       closureType: null,
       description: 'The example type.',
       noToc: false,
       spread: false,
       import: false,
       noExpand: false,
       link: null,
       properties: [],
       namespace: null,
       isConstructor: false,
       isInterface: false,
       isRecord: false,
       extends: null },
     Type {
       name: 'GoodMorning',
       type: 'HelloWorld',
       closureType: 'ns.HelloWorld',
       description: 'Life is seeing sunlight every day.',
       noToc: false,
       spread: false,
       import: false,
       noExpand: false,
       link: null,
       properties: [],
       namespace: null,
       isConstructor: false,
       isInterface: false,
       isRecord: false,
       extends: null },
     Type {
       name: 'Conf',
       type: null,
       closureType: null,
       description: 'The configuration object',
       noToc: false,
       spread: false,
       import: false,
       noExpand: false,
       link: null,
       properties: 
        [ Property {
            name: 'propName',
            description: 'The property description.',
            type: 'HelloWorld',
            closureType: 'ns.HelloWorld',
            hasDefault: false,
            default: null,
            optional: false } ],
       namespace: null,
       isConstructor: false,
       isInterface: false,
       isRecord: false,
       extends: null } ],
  imports: [],
  Imports: [] }

Optional And Default

  • Optional (opt) means that the property of a type can be undefined.
  • Default (default) means that when not given, the property will take the default value.
  • In configs, default implies optional. However, in other types, it does not have to be so.
  • Currently, default will trigger optional. Possibly fix that and make specifying optionals implicit.
Art Deco © Art Deco 2019 Tech Nation Visa Tech Nation Visa Sucks

Keywords

FAQs

Package last updated on 22 Jul 2019

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc