May 07, 2021 TypeScript
1. All the validators are in one file
2. A validator that uses a namespace
Note about terminology: It is important to note that the term name in TypeScript 1.5 has changed. T he "internal module" is now called the "namespace". "External Module" is now referred to simply as "Module" in order to be consistent with the terminology in ECMAScript 2015 (i.e.,
module X {
is equivalent to the nownamespace X {
This article describes how to organize your code in TypeScript using namespaces (previously called "internal modules").
As we mentioned in the terminology note, "internal modules" are now called "namespaces".
In addition, any place that uses the
module
keyword to declare an internal module should use
namespace
keyword between them.
This avoids confusing new consumers with similar names.
Let's start with a program and use this example throughout the article. Let's define a few simple string validators, assuming you use them to validate user input or external data in the form.
interface StringValidator {
isAcceptable(s: string): boolean;
}
let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;
class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (let name in validators) {
console.log(""" + s + "" " + (validators[name].isAcceptable(s) ? " matches " : " does not match ") + name);
}
});
As more validators are added, we need a way to organize the code so that we don't have to worry about naming conflicts with other objects while recording their types. So instead of putting them under the global namespace, we wrap the validators in a namespace.
In the following example, put all the types associated with the validator in a namespace called
Validation
B
ecause we want these interfaces and classes to be accessible outside the namespace, we need to use
export
C
onversely,
lettersRegexp
numberRegexp
implementation details that do not need to be exported, so they are inalible outside the namespace.
In the test code at the end of the file, you need to qualify the name of a type, such as
Validation.LettersOnlyValidator
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
});
As applications get bigger and bigger, we need to separate the code into different files for easy maintenance.
Now, let's
Validation
namespace into multiple files. A
lthough they are different files, they are still the same namespace and are used as if they were defined in a file. B
ecause there are dependencies between different files, we have added reference labels to tell the compiler files about the association.
Our test code remains the same.
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
/// <reference path="Validation.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}
/// <reference path="Validation.ts" />
namespace Validation {
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (let name in validators) {
console.log(""" + s + "" " + (validators[name].isAcceptable(s) ? " matches " : " does not match ") + name);
}
});
When it comes to multiple files, we have to make sure that all the compiled code is loaded. We have two ways.
The first way to compile all the input files into one output file requires a
--outFile
tag:
tsc --outFile sample.js Test.ts
The compiler automatically sorts the output based on the reference labels in the source code. You can also specify each file individually.
tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts
The second way, we can compile each file (the default), and each source file generates a JavaScript file accordingly.
Then, on the
<script>
are introduced in the correct order through the hashtag, such as:
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />
Another way to simplify namespace operations is to
import q = x.y.z
give commonly used objects a short name. D
on't confuse it with
import x = require('name')
which here creates an alias for the specified symbol.
You can create aliases for any identifier in this way, including objects in imported modules.
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}
import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // Same as "new Shapes.Polygons.Square()"
Note that instead of using
require
keyword, we use the qualified name assignment of the import symbol directly. T
his is similar to
var
but it also applies to types and imported symbols with namespace meaning.
Importantly, for values,
import
different reference than the original symbol, so changing the
var
does not affect the value of the original variable.
In order to describe the type of class library that is not written in TypeScript, we need to declare the API exported by the class library. Because most libraries provide only a few top-level objects, namespaces are a good way to represent them.
We call it a declaration because it is not a specific implementation of an external program. W
e usually
.d.ts
I
f you're familiar with C/C, you can think of them as
.h
files.
Let's look at some examples.
The popular library D3 defines its
d3
global object d3. B
ecause the library is
<script>
(not through a module loader), its declaration file uses internal modules to define its type. I
n order for the TypeScript compiler to recognize its type, we use an external namespace declaration.
For example, we can write something like this:
declare namespace D3 {
export interface Selectors {
select: {
(selector: string): Selection;
(element: EventTarget): Selection;
};
}
export interface Event {
x: number;
y: number;
}
export interface Base extends Selectors {
event: Event;
}
}
declare let d3: D3.Base;