127 lines
2.8 KiB
Plaintext
127 lines
2.8 KiB
Plaintext
A fluent interface designates an object-oriented API relying on method chaining to improve code readability.
|
|
In such interfaces, methods return the object `this` to allow the caller to chain multiple method invocations.
|
|
|
|
[source,javascript]
|
|
----
|
|
class RichText {
|
|
constructor(private readonly text: string) {}
|
|
bold(): RichText {
|
|
// [...]
|
|
return this;
|
|
}
|
|
italic(): RichText {
|
|
// [...]
|
|
return this;
|
|
}
|
|
}
|
|
|
|
const richText = new RichText('Hello, World!');
|
|
// Chaining methods bold() and italic().
|
|
console.log(richText.bold().italic());
|
|
----
|
|
|
|
To better support fluent interfaces when used with a hierarchy of classes,
|
|
TypeScript provides a special type `this` that refers dynamically to the type of the current class.
|
|
|
|
Methods returning `this` should thus use the corresponding special type `this`
|
|
instead of the class name in their signatures.
|
|
|
|
== Why is this an issue?
|
|
|
|
When a method return type is the declaring class name in a hierarchy of classes,
|
|
it is impossible to chain methods defined in the superclass with methods defined in subclasses.
|
|
|
|
[source,javascript]
|
|
----
|
|
enum Color {
|
|
RED, BLUE, GREEN
|
|
}
|
|
|
|
class Shape {
|
|
// The return type is the class name.
|
|
move(x: number, y: number): Shape {
|
|
// [...]
|
|
return this;
|
|
}
|
|
}
|
|
|
|
class Polygon extends Shape {
|
|
fill(color: Color): Polygon {
|
|
// [...]
|
|
return this;
|
|
}
|
|
}
|
|
|
|
const polygon = new Polygon();
|
|
polygon.move(1.0, 2.0).fill(Color.RED);
|
|
// ^^^^
|
|
// Property 'fill' does not exist on type 'Shape'.
|
|
----
|
|
|
|
== How to fix it
|
|
|
|
When a method declaration uses the special type `this` instead of the class name for its return type,
|
|
TypeScript will use the type of the object `this` instead of the method declaring class
|
|
and will accept to invoke methods defined in the object class:
|
|
|
|
[source,javascript]
|
|
----
|
|
enum Color {
|
|
RED, BLUE, GREEN
|
|
}
|
|
|
|
class Shape {
|
|
// The return type is now "this"
|
|
move(x: number, y: number): this {
|
|
// [...]
|
|
return this;
|
|
}
|
|
}
|
|
|
|
class Polygon extends Shape {
|
|
fill(color: Color): this {
|
|
// [...]
|
|
return this;
|
|
}
|
|
}
|
|
|
|
const polygon = new Polygon();
|
|
polygon.move(1.0, 2.0).fill(Color.RED); // No compilation error
|
|
----
|
|
|
|
=== Code examples
|
|
|
|
==== Noncompliant code example
|
|
|
|
[source,javascript,diff-id=1,diff-type=noncompliant]
|
|
----
|
|
class A {
|
|
foo(): A { // Noncompliant
|
|
return this;
|
|
}
|
|
bar = (): A => { // Noncompliant
|
|
return this;
|
|
};
|
|
}
|
|
----
|
|
|
|
==== Compliant solution
|
|
|
|
[source,javascript,diff-id=1,diff-type=compliant]
|
|
----
|
|
class A {
|
|
foo(): this {
|
|
return this;
|
|
}
|
|
bar = (): this => {
|
|
return this;
|
|
};
|
|
}
|
|
----
|
|
|
|
== Resources
|
|
=== Documentation
|
|
|
|
* TypeScript Documentation - https://www.typescriptlang.org/docs/handbook/2/classes.html#this-types[``++this++`` Types]
|
|
* Wikipedia - https://en.wikipedia.org/wiki/Fluent_interface[Fluent interface]
|