Typal
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:
- Manage types from an external XML location.
- Compile types for JSDoc compatible both with GCC and VSCode.
- Compile types as externs for GCC and use in other packages.
- Place types' descriptions as formatted tables in markdown (used in Documentary).
- 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 {
constructor(rule, options) {
super(options)
this.rule = rule
}
_transform(chunk, enc, next) {
this.push(
`${chunk}`.replace(this.rule.regex, this.rule.replacer)
)
next()
}
}
|
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:
- Google Closure Compiler does not understand typedefs a) without
var
iable declaration underneath, b) with @prop
erties 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.
var Rule
- 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'
const fn = (rule) => {}
- 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. - 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 {
constructor(rule, options) {
super(options)
this.rule = rule
}
}
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 {
constructor(rule, options) {
super(options)
this.rule = rule
}
}
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.
Name | Type | Description |
---|
regex* | RegExp | The regular expression. |
replacement* | (...args:string) => string | The 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:
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:
for more information.
* @typedef {import('stream').TransformOptions} TransformOptions
^
restream/index2.js:26: WARNING - Bad type annotation. type annotation incompatible with
other annotations.
See https:
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>
|
- Annotate the nullability of our types using !, since there's attention to
null in GCC, not like traditional JS. - 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. - 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. - 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 {
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:
|
|
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.
|
|
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.
|
|
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) |
---|
var _restream = {}
_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 /Volumes/backup/closure-compiler/target/closure-compiler-1.0-SNAPSHOT.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/src/node_modules/@depack/externs/v8/stream.js --externs \
../../depack/src/node_modules/@depack/externs/v8/events.js --externs \
../../depack/src/node_modules/@depack/externs/v8/global.js --externs \
../../depack/src/node_modules/@depack/externs/v8/global/buffer.js --externs \
../../depack/src/node_modules/@depack/externs/v8/nodejs.js
Modules: example/restream/compat.js
Built-ins: stream
Running Google Closure Compiler target...
|
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'
const rule = {
regex: /__(.+?)__/,
replacement(match, s) {
return `<em>${s}</em>`
},
}
const restream = new Restream(rule)
restream.pipe(process.stdout)
restream.end('__hello world__')
|
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:
-
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
|
const prog = (conf, options) => {}
|
-
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
|
const prog = (conf, options) => {}
|
-
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
|
var _typal = {}
_typal.Conf
|
Typal Arguments
$ typal source [--closure|externs] [-o output] [-vh]
The following arguments are supported by this software.
Argument | Short | Description |
---|
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 | -h | Print the help information and exit. |
--version | -v | Show 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() {}
_
- 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.
function example (
writable, readable,
type, missingType,
array, promise, object, union,
error,
string,
) {}
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:
export {}
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:
function example(config = {}) {
const { test } = config
}
function example(config = {}) {
const { test } = 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 |
---|
|
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.
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
var _test = {}
_test.Test
_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) |
---|
|
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). |
var _test = {}
_test.Test
_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) |
---|
|
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). |
|
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 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)
|
---|
|
In standard mode, Typal does not use namespaces. |
|
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.
Name | Type | Description |
---|
type | ?Type | The type itself, possibly nullable. |
union | !(Type | string) | The union of types. |
record | { t: Type, r } | The record with a type. |
application | Object<string, Type> | The application with a type. |
function | function(this: Type, string, !Type): Type | The function with arguments and return type. |
variable-args | function(...Type) | Functions with ... for variable argument types. |
vscode-function | (type: Type, s: string) => Type | Linking 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.
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.
Copyright