May 07, 2021 TypeScript
One of typeScript's core principles is to type check the shape that the value has. I t is sometimes referred to as "duck-type" or "structural subtype". In TypeScript, interfaces are designed to name these types and define contracts for your code or third-party code.
Here's a simple example to see how the interface works:
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
The type checker views
printLabel
printLabel
an argument and requires this object argument to have a property named
label
string
string. I
t is important to note that the object parameters we pass in actually contain many properties, but the compiler only checks for the existence of those required properties and whether their types match.
However, there are times when TypeScript isn't so loose, and we'll cover it a little bit below.
Let's override the example above, this time using an interface to describe that you must include a
label
property and the type
string
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
LabelledValue
interface is like a name that describes the requirements in the example above. I
t represents an object with
label
property and a type
string
I
t's important to note that we can't implement this interface here with objects that are
printLabel
do in other languages. W
e're just going to look at the shape of the value.
An incoming object is allowed as long as it meets the necessary conditions mentioned above.
It is also worth noting that the type inspector does not check the order of the properties, as long as the corresponding properties exist and the type is correct.
Not all of the properties in the interface are required. S ome exist only under certain conditions, or do not exist at all. Optional properties are commonly used when applying "option bags" mode, where only some properties are assigned to parameter objects passed in by functions.
Here's an example of applying "option bags":
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
Interfaces with optional properties are similar to normal interface definitions, with just one
?
Symbol.
One of the benefits of optional properties is that you can predefine properties that may exist, and the second is that you can catch errors that refer to properties that do not exist.
For example, if we deliberately misspelled the color property name in
createSquare
we'll get an error prompt:
color
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.color) {
// Error: Property 'collor' does not exist on type 'SquareConfig'
newSquare.color = config.collor; // Type-checker can catch the mistyped name here
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
Some object properties can only modify the value of an object when it was first created.
You can specify read-only
readonly
before the property name:
interface Point {
readonly x: number;
readonly y: number;
}
You can construct a Point by assigning a literal amount of an
Point
After the assignment,
x
and
y
longer be changed.
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
TypeScript has
ReadonlyArray<T>
which is similar to
Array<T>
it just removed all the variable methods, so you can make sure that the array can no longer be modified after it is created:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
On the last line of the code above, you can see that even assigning the entire
ReadonlyArray
to a normal array is not okay.
But you can override with type assertions:
a = ro as number[];
readonly
vs
const
The easiest way to
readonly
or
const
is to use it as a variable or as a property.
const
if used as a variable and
readonly
We used the interface in the first example, typeScript let's pass in
{ size: number; label: string; }
expect only the
{ label: string; }
function of the .
We've learned about optional properties and know they're useful in "option bags" mode.
However, naively combining the two would be like lifting a stone and throwing a stone at your own feet, as in JavaScript.
For example,
createSquare
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });
Note that the
createSquare
are spelled as
colour
instead of
color
In JavaScript, this fails silently.
You might argue that the program has been typed correctly because
width
is compatible,
color
property, and the extra
colour
property is meaningless.
However, TypeScript will think that there may be a bug in this code. O bject literals are treated specially and subject to additional property checks when they are assigned to variables or passed as arguments. If an object literally has any properties that are not included in the Target Type, you get an error.
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });
It's easy to get around these checks. The easiest way is to use type assertions:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
However, the best way is to be able to add a string index signature, provided that you are able to determine that the object may have some additional properties for special purpose use.
If
SquareConfig
the color and width properties
color
width
above, and also
has any number of other properties, we can define it this way:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
We'll talk about index signatures later, but what we're saying here
SquareConfig
have any number of properties, and as long as
color
and
width
it doesn't matter what their type is.
There's one last way to skip these checks, which might surprise you by assigning this object to another
squareOptions
compiler doesn't report errors.
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
Keep in mind that in simple code like the one above, you probably shouldn't go around these checks. Y
ou may need to use these techniques literally for complex objects that contain methods and internal states, but most additional property check errors are real bugs. T
hat means you've encountered additional type check errors, such as selecting a package, and you should review your type declaration.
Here, if you support passing
color
or
colour
createSquare
you should modify the
SquareConfig
definition to reflect this.
Interfaces can describe the various shapes that objects in JavaScript have. In addition to describing normal objects with properties, interfaces can also describe function types.
In order to use an interface to represent a function type, we need to define a call signature for the interface. I t's like a function definition with only a list of parameters and a return value type. Each argument in the argument list requires a name and type.
interface SearchFunc {
(source: string, subString: string): boolean;
}
With this definition, we can use this function type interface just like any other interface. The following example shows how to create a function type variable and assign a function of the same type to that variable.
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
if (result == -1) {
return false;
}
else {
return true;
}
}
For type checking of function types, the parameter names of functions do not need to match the names defined in the interface. For example, let's rewrite the example above with the following code:
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
The parameters of the function are checked one by one, requiring that the parameter types at the corresponding locations be compatible. I
f you don't want to specify a type, Typescript's type system infers the argument type because the function is assigned directly
SearchFunc
type variable. T
he return value type of a function is inferred from its return value (in this
false
and
true
If you let this function return a number or string, the type inspector warns us that the return value type of the function does not match the definition in the
SearchFunc
interface.
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
if (result == -1) {
return false;
}
else {
return true;
}
}
Similar to using interfaces to describe function types, we can also describe types that can be "indexed", such as
a[10]
ageMap["daniel"]
T
he indexable type has an
index signature that describes
the type of object index and the corresponding index return value type.
Let's look at an example:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
In the example above, we defined
StringArray
interface, which has an index signature.
This index signature indicates that when you index
string
StringArray
get a return value of string type.
number
There are two types of index signatures supported: strings and numbers. B
oth types of indexes can be used at the same time, but the return value of the numeric index must be a sub-type of the string index return value type. T
his is because
number
use number to index, JavaScript converts it to string and
string
goes to index the object.
That
100
number
is
"100"
string
so the two need to be consistent.
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// Error: indexing with a 'string' will sometimes get you a Dog!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
String index signatures are a good description of
dictionary
pattern, and they also ensure that all properties match the type of value they return. B
ecause the string index
obj.property
and
obj["property"]
are available.
In the following example,
name
does not match the type of string index, so the type inspector gives an error prompt:
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型不是索引类型的子类型
}
Finally, you can set the index signature to read-only, which prevents the index from being assigned a value:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
You
myArray[2]
the index signature is read-only.
TypeScript can also be used to explicitly force a class to conform to a contract, as is the basic function of an interface in C# or Java.
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
You can also describe a method in an interface and implement it in a class, just like
setTime
method below:
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
Interfaces describe the public part of a class, not the public and private parts. It does not help you check if the class has some private members.
When you manipulate classes and interfaces, you need to know that classes have two types: the type of static part and the type of instance. You'll notice that when you define an interface with a constructor signature and try to define a class to implement the interface, you get an error:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
This is because when a class implements an interface, only its instance part is type checked. Constructor exists in the static part of the class, so it is not within the scope of the check.
Therefore, we should directly manipulate the static part of the class. L
ooking at the following example, we define two
ClockConstructor
for constructors and
ClockInterface
for instance methods.
To make it easier for us to define a
createClock
which creates instances from incoming types.
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
Because the first argument of
createClock
is the
ClockConstructor
createClock(AnalogClock, 7, 32)
AnalogClock
is checked for conformity with constructor signatures.
Like classes, interfaces can scale to each other. This allows us to copy members from one interface to another, giving us more flexibility to split interfaces into reusable modules.
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
An interface can inherit multiple interfaces and create a composite interface for multiple interfaces.
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
As we mentioned earlier, interfaces can describe rich types in JavaScript. Because JavaScript is dynamic and flexible, sometimes you want an object to have multiple types mentioned above at the same time.
An example is an object that can be used as both a function and an object with additional properties.
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
When using JavaScript third-party libraries, you may need to define the type in its entirety, as above.
When an interface inherits a class type, it inherits the members of the class but does not include its implementation. I t is as if the interface declares the members that exist in all classes, but does not provide a specific implementation. I nterfaces also inherit private and protected members of the class. This means that when you create an interface that inherits a class that has private or protected members, the interface type can only be implemented by that class or its sub-classes.
This is useful when you have a very deep inheritance, but just want your code to work only for sub-classes that have specific properties. S ub-classes have no connection to the base class except 10 inherited from the base class. Cases:
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
select() { }
}
// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
select() { }
}
class Location {
}
In the example above,
SelectableControl
all members of
Control
including the private
state
B
ecause
state
is a private member, selectableControl
Control
can only be implemented by
SelectableControl
Because only
Control
sub-classes can have a
Control
on
state
this is required for the compatibility of private members.
Within
Control
class, private member state is allowed to be accessed through an instance of
SelectableControl
state
I
n fact,
SelectableControl
is like
Control
and has a
select
method.
Button
and
TextBox
SelectableControl
(because they both
Control
and have select
select
but
Image
and
Location
are not.