TypeScript is, like its base-languages ES6 and JavaScript, a very flexible language. You can express things in many different ways. This provides great freedom for developers that write new source code, but lots of efforts for those that have to read and understand it on maintenance. In that way, TypeScript reminds me a little bit of Scala, although it (fortunately) has no operator overloading. Programming languages that allow so much freedom of expression are sure not made for common sense. Mind that common sense is one of the main goals of Scrum!
I am a Java developer.
For this Blog I tried to implement the interfaces and classes
Point
, Dimension
and Rectangle
in TypeScript,
separating interfaces and implementations in an
OO way.
Following are my findings when thinking in Java but implementing in TypeScript.
Both support interfaces and classes.
Both support only single inheritance in classes, but multiple inheritance in interfaces.
Type Assignments
They go after the variable or parameter name. It is not
Point origin; // Java
In TypeScript (and all newer programming languages like Scala or Kotlin) this has become
origin: Point; // TypeScript
So the type of the field goes after the name, separated by a colon. This applies also to functions:
function foobar(foo: string, bar: number): boolean { return true; }
For standalone functions (outside any class) you need the function
keyword.
Inside classes you must drop it.
Members Always Need this
It was called "member" in C++, and "field" in Java. I am talking about width and height in following TypeScript class:
class DimensionImpl { width: number; height: number; .... area(): number { return this.width * this.height; } }
Look at the area()
implementation, and how class fields are accessed.
Other than in Java there is no implicit "this".
You always need to write this.someField
when referring to a member field,
or this.someFunction()
for a member method.
Fortunately the compiler will remind you when you forget it. This is why software developers like compilers: they tell you your mistakes before they happen at runtime!
Interface Fields are not Constants
Constants in TypeScript interfaces are not possible. Why would I like to have constants in interfaces? Because a method of that interface may return a constant that the caller needs to recognize, and I would like to keep such constants together with the method. Same for parameters.
Both Java and TypeScript allow to have fields in interfaces. But Java interface fields are constants, while TypeScript interface fields are variables by default, and can not be initialized in the interface.
interface Colors { GREEN: string = "Green"; }
This will give you a compile error:
error TS1246: An interface property cannot have an initializer.
So then, let's see if we can have constant fields in a class:
interface Dimension { width: number; height: number; } class DimensionImpl implements Dimension { const width: number; // compile error! const height: number; // compile error! .... }
Trying to translate this, the compiler will tell you
error TS1248: A class member cannot have the 'const' keyword.
Now you can do it the Java way:
interface Dimension { getWidth(): number; getHeight(): number; } class DimensionImpl implements Dimension { private width: number; private height: number; .... getWidth(): number { return this.width; } getHeight(): number { return this.height; } }
Or you can investigate and do it how TypeScript does immutable fields:
interface Dimension { readonly width: number; readonly height: number; } class DimensionImpl implements Dimension { readonly width: number; readonly height: number; .... }
Of course readonly
is not intuitive
when having the ES6 const
keyword that is responsible for immutability.
No Real Function Overloading
Try to compile following TypeScript code:
interface Point { x: number; y: number; distance(): number; distance(other: Point): number; } class PointImpl implements Point { readonly x: number; readonly y: number; .... distance(): number { return this.distance(new PointImpl(0, 0)); } distance(other: Point): number { const distanceX = this.distanceX(other); const distanceY = this.distanceY(other); return Math.sqrt(distanceX * distanceX + distanceY * distanceY); } .... // distanceX() and distanceY() implementations }
You will get following compile error from class PointImpl
(not from interface Point
):
error TS2393: Duplicate function implementation.
So is the promise of function overloading in TypeScript a hoax? Not completely. You need to provide everything in just one implementation, and forward to that via empty function signatures:
interface Point { x: number; y: number; distance(): number; distance(other: Point): number; } class PointImpl implements Point { readonly x: number; readonly y: number; .... distance(): number; // you could also leave this out completely! distance(other?: Point): number { if ( ! other ) other = new PointImpl(0, 0); const distanceX = this.distanceX(other); const distanceY = this.distanceY(other); return Math.sqrt(distanceX * distanceX + distanceY * distanceY); } .... // distanceX() and distanceY() implementations }
For sure this is a surprise for OO programmers.
Mind the question mark after the parameter other
.
It makes the parameter nullable.
This is the TypeScript way of function overloading.
TypeScript Example Implementation
So here are my TypeScript interfaces and classes for
Point
, Dimension
and Rectangle
.
The interface Rectangle
extends both Point
and Dimension
.
The implementation RectangleImpl
extends PointImpl
and holds a Dimension
delegate,
because it can extend just one super-class (TypeScript single inheritance),
but needs to implement Dimension
.
Thus it must somehow duplicate the Dimension
interface.
I had all files in just one directory, and compiled them with:
tsc -t ES6 *.ts
Interfaces
Dimension.ts
export interface Dimension { readonly width: number; readonly height: number; area(): number; }
Point.ts
export interface Point { readonly x: number; readonly y: number; distance(): number; distance(other: Point): number; transform(origin: Point): Point; }
Rectangle.ts
import { Point } from "./Point.js"; import { Dimension } from "./Dimension.js"; export interface Rectangle extends Point, Dimension { move(origin: Point): Rectangle; resize(newDimension: Dimension): Rectangle; }
Classes
DimensionImpl.ts
import { Dimension } from "./Dimension.js"; export class DimensionImpl implements Dimension { readonly width: number; readonly height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } area(): number { return this.width * this.height; } }
PointImpl.ts
import { Point } from "./Point.js"; export class PointImpl implements Point { readonly x: number; readonly y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } distance(other?: Point): number { if ( ! other ) other = new PointImpl(0, 0); const distanceX = this.distanceX(other); const distanceY = this.distanceY(other); return Math.sqrt(distanceX * distanceX + distanceY * distanceY); } transform(origin: Point): Point { return new PointImpl(this.distanceX(origin), this.distanceY(origin)); } private distanceX(other: Point): number { return this.x - other.x; } private distanceY(other: Point): number { return this.y - other.y; } }
RectangleImpl.ts
import { Point } from "./Point.js"; import { PointImpl } from "./PointImpl.js"; import { Dimension } from "./Dimension.js"; import { Rectangle } from "./Rectangle.js"; export class RectangleImpl extends PointImpl implements Rectangle { private readonly dimension: Dimension; constructor(origin: Point, dimension: Dimension) { super(origin.x, origin.y); this.dimension = dimension; } // START Dimension delegates width: number = this.dimension.width; height: number = this.dimension.height; area(): number { return this.dimension.area(); } // END Dimension delegates resize(newDimension: Dimension): Rectangle { return new RectangleImpl(this, newDimension); } move(newOrigin: Point): Rectangle { return new RectangleImpl(newOrigin, this.dimension); } }
Mind that the imports name .js
files to be ES6-compatible.
It would not work with .ts
files.
It would work with no extension at all, but this is not ES6-compatible.
In other words, the TypeScript compiler, in ES6 mode, analyses the dependency tree and compiles bottom-up!
A Strange Compile Result
I just compiled these sources, this worked, but I did not test them yet. As much as I see now, the compilation goes horribly wrong sometimes. When using the compiled classes, I get a runtime (!) error
TypeError: this.dimension is undefined
from RectangleImpl.js
line 6:
1 2 3 4 5 6 7 8 9 10 11 | import { PointImpl } from "./PointImpl.js"; export class RectangleImpl extends PointImpl { constructor(origin, dimension) { super(origin.x, origin.y); // START Dimension delegates this.width = this.dimension.width; this.height = this.dimension.height; this.dimension = dimension; } .... } |
The bug is obvious: this.width = this.dimension.width
can not work when this.dimension = dimension
is done two lines later!
This has been compiled by TypeScript from following source (snippet):
export class RectangleImpl extends PointImpl implements Rectangle { private readonly dimension: Dimension; constructor(origin: Point, dimension: Dimension) { super(origin.x, origin.y); this.dimension = dimension; } // START Dimension delegates width: number = this.dimension.width; height: number = this.dimension.height; .... }
I hoped that I can forward x
and y
to dimension
that way,
but there seems to be no easy solution for forwarding properties to a delegate object.
Will this be fixed by the TypeScript team?
Meanwhile I fixed this by storing the values (redundantly) in constructor:
export class RectangleImpl extends PointImpl implements Rectangle { readonly width: number; readonly height: number; private readonly dimension: Dimension; constructor(origin: Point, dimension: Dimension) { super(origin.x, origin.y); this.dimension = dimension; this.width = dimension.width; this.height = dimension.height; } .... }
The problem is obvious: the value for width
is now stored two times,
once in the delegate dimension
, once in RectangleImpl
.
Readonly Must Be in Interface
Having the readonly
keyword only on the class property
is not enough to make the compiler check for write accesses.
Following code will not give you a compile error due to assigning dimension.width
a new value:
interface Dimension { width: number; height: number; } class DimensionImpl implements Dimension { readonly width: number; readonly height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } } const dimension: Dimension = new DimensionImpl(1, 2); dimension.width = 999; // NOT a compile error!!!
You need to state readonly
also in the interface:
interface Dimension { readonly width: number; readonly height: number; } .... const dimension: Dimension = new DimensionImpl(1, 2); dimension.width = 999; // the needed compile error.
Now we get the necessary access check:
error TS2540: Cannot assign to 'width' because it is a constant or a read-only property.
Conclusion
Some techniques that look promising and are accepted by the compiler seem to cause troubles at runtime. That means unit tests are indispensable also with TypeScript, albeit of strong type checks.
Another big problem seems to be dependency management.
I could not make my ES6 imports run with nodejs
.
It gave me SyntaxError: Unexpected token import
.
Installing the
babel
transpiler libraries did not help so far (working on it).
There are many more differences between Java and TypeScript concerning interfaces and classes,
I just mentioned a few, and will continue to blog them.
For example, TypeScript interfaces can
determine constructors
(see ClockConstructor
).
Integrating a new language that provides so much freedom makes common life hard. Where are the programming languages that provide just what's needed, and thus support common sense actively?
1 Kommentar:
It is nice blog Thank you provide important information and I am searching for the same information to save my time AngularJS Online Training
Kommentar veröffentlichen