Preface
The official documentation of TypeScript has long been updated, but the Chinese documents I can find are still in the older version. Therefore, some new and revised chapters have been translated and sorted out.
This article is organized from the " Object Types " chapter in the TypeScript Handbook.
This article does not strictly follow the original translation, but also explains and supplements part of the content.
Object types
In JavaScript, the most basic way to group and distribute data is through objects. In TypeScript, we describe objects by object types.
The object type can be anonymous:
function greet(person: { name: string; age: number }) {
return "Hello " + person.name;
}
You can also use the interface to define:
interface Person {
name: string;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
}
Or through type aliases:
type Person = {
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}
Property Modifiers
Each attribute in the object type can indicate its type, whether the attribute is optional, whether the attribute is read-only, and other information.
Optional Properties
?
mark after the attribute name to indicate that this attribute is optional:
interface PaintOptions {
shape: Shape;
xPos?: number;
yPos?: number;
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
In this example, xPos
and yPos
are optional attributes. Because they are optional, all the above calling methods are legal.
We can also try to read these attributes, but if we are in strictNullChecks
mode, TypeScript will prompt us that the attribute value may be undefined
.
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos;
// (property) PaintOptions.xPos?: number | undefined
let yPos = opts.yPos;
// (property) PaintOptions.yPos?: number | undefined
}
In JavaScript, if an attribute value is not set, we get undefined
. So we can deal with it specially undefined
function paintShape(opts: PaintOptions) {
let xPos = opts.xPos === undefined ? 0 : opts.xPos;
// let xPos: number
let yPos = opts.yPos === undefined ? 0 : opts.yPos;
// let yPos: number
}
This kind of judgment is so common in JavaScript that it provides special syntactic sugar:
function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
console.log("x coordinate at", xPos); // (parameter) xPos: number
console.log("y coordinate at", yPos); // (parameter) yPos: number
// ...
}
Here we use destructuring syntax and provide default values xPos
and yPos
Now the values of xPos
and yPos
must exist inside the paintShape
function, but they are optional paintShape
Note that there is no way to put type annotations in the destructuring grammar. This is because in JavaScript, the meaning of the following syntax is completely different.
function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
render(shape);
// Cannot find name 'shape'. Did you mean 'Shape'?
render(xPos);
// Cannot find name 'xPos'.
}
In the object deconstruction grammar, shape: Shape
represents the assignment of the value of shape
Shape
. xPos: number
is the same, it will create a variable number
xPos
readonly
property (readonly Properties)
In TypeScript, attributes can be marked as readonly
, which will not change any runtime behavior, but during type checking, an readonly
cannot be written.
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
// We can read from 'obj.prop'.
console.log(`prop has the value '${obj.prop}'.`);
// But we can't re-assign it.
obj.prop = "hello";
// Cannot assign to 'prop' because it is a read-only property.
}
However, the use of readonly
does not mean that a value is completely unchanged, or that the internal content cannot be changed. readonly
only indicates that the attribute itself cannot be rewritten.
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
// We can read and update properties from 'home.resident'.
console.log(`Happy birthday ${home.resident.name}!`);
home.resident.age++;
}
function evict(home: Home) {
// But we can't write to the 'resident' property itself on a 'Home'.
home.resident = {
// Cannot assign to 'resident' because it is a read-only property.
name: "Victor the Evictor",
age: 42,
};
}
When TypeScript checks whether two types are compatible, it does not consider whether the attributes in the two types are readonly
, which means that readonly
can be modified through aliases.
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
Index Signatures
Sometimes, you cannot know the names of all the attributes in a type in advance, but you know the characteristics of these values.
In this case, you can use an index signature to describe the possible value types, for example:
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1]; // const secondItem: string
In this way, we have an interface StringArray
with an index signature. This index signature indicates that when a StringArray
type number
string
type 061d45ccc38a40, it will return a value of type 061d45ccc38a4d.
The attribute type of an index signature must be string
or number
.
Although TypeScript can support both string
and number
, the return type of the numeric index must be a subtype of the return type of the character index. This is because when a number is used for indexing, JavaScript actually turns it into a string. This means that indexing with the number 100 is the same as indexing with the string 100.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
// 'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
[x: string]: Dog;
}
Although the string index is very effective for describing the dictionary pattern, it will also force all attributes to match the return type of the index signature. This is because a similar statement obj.property
string index, with the obj["property"]
is the same. In the following example, name
does not match the type of string index, so the type checker will give an error:
interface NumberDictionary {
[index: string]: number;
length: number; // ok
name: string;
// Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
However, if an index signature is a union of attribute types, then various types of attributes can be accepted:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
Finally, you can also set the index signature to readonly
.
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
// Index signature in type 'ReadonlyStringArray' only permits reading.
Because the index signature is readonly
, you cannot set the value of myArray[2]
Property inheritance (Extending Types)
Sometimes we need a type that is more specific than other types. For example, suppose we have a BasicAddress
type to describe the required fields for mailing letters and packages in the United States.
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
This has been met in some cases, but buildings at the same address often have different unit numbers. We can write another AddressWithUnit
:
interface AddressWithUnit {
name?: string;
unit: string;
street: string;
city: string;
country: string;
postalCode: string;
}
This is fine to write, but in order to add a field, it is necessary to completely copy it again.
We can change it to inherit BasicAddress
to achieve:
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
extends
keyword for the interface allows us to effectively copy members from other declared types and add new members at will.
Interfaces can also inherit multiple types:
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
Intersection Types
TypeScript also provides a method called Intersection types, which is used to merge existing object types.
The definition of the cross type requires the use of the &
operator:
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
Here, we connect Colorful
and Circle
produce a new type, the new type has all members of Colorful
and Circle
function draw(circle: Colorful & Circle) {
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });
// oops
draw({ color: "red", raidus: 42 });
// Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'.
// Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?
Interface inheritance and cross type (Interfalces vs Intersections)
These two methods look very similar in the type of merging, but in fact there is still a big difference. The most principled difference lies in how the conflict is handled, which is the main reason why you decide to choose that method.
interface Colorful {
color: string;
}
interface ColorfulSub extends Colorful {
color: number
}
// Interface 'ColorfulSub' incorrectly extends interface 'Colorful'.
// Types of property 'color' are incompatible.
// Type 'number' is not assignable to type 'string'.
Using inheritance, if you rewrite the type, it will cause a compilation error, but the cross type will not:
interface Colorful {
color: string;
}
type ColorfulSub = Colorful & {
color: number
}
Although no error will be reported, what is the type of the attribute of color
never
, which is the intersection string
and number
Generic Object Types
Let us write such a Box
, which can contain any value:
interface Box {
contents: any;
}
Now content
type attribute is any
, can be used, but prone to rollovers.
We can also use unknown
instead, but this also means that if we already know the contents
, we need to do some precautionary checks, or use an error-prone type assertion.
interface Box {
contents: unknown;
}
let x: Box = {
contents: "hello world",
};
// we could check 'x.contents'
if (typeof x.contents === "string") {
console.log(x.contents.toLowerCase());
}
// or we could use a type assertion
console.log((x.contents as string).toLowerCase());
A safer approach is to Box
according to contents
some more specific types of split:
interface NumberBox {
contents: number;
}
interface StringBox {
contents: string;
}
interface BooleanBox {
contents: boolean;
}
But this also means that we have to create different functions or function overloads to handle different types:
function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
}
This is too cumbersome to write.
So we can create a generic Box
, which declares a type parameter:
interface Box<Type> {
contents: Type;
}
You can interpret it this way: Box
of Type
is contents
type have Type
.
When we quote Box
, we need to give a type argument to replace Type
:
let box: Box<string>;
Think of Box
as an actual type of template. Type
is a placeholder that can be replaced with a specific type. When TypeScript sees Box<string>
, it will replace Box<Type>
with Type
as string
, and the final result will become { contents: string }
. In other words, Box<string>
and StringBox
are the same.
interface Box<Type> {
contents: Type;
}
interface StringBox {
contents: string;
}
let boxA: Box<string> = { contents: "hello" };
boxA.contents;
// (property) Box<string>.contents: string
let boxB: StringBox = { contents: "world" };
boxB.contents;
// (property) StringBox.contents: string
But the current Box
is reusable. If we need a new type, we don't need to declare a new type at all.
interface Box<Type> {
contents: Type;
}
interface Apple {
// ....
}
// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>;
This also means that we can use generic functions to avoid the use of function overloading.
function setContents<Type>(box: Box<Type>, newContents: Type) {
box.contents = newContents;
}
Type aliases can also use generics. for example:
interface Box<Type> {
contents: Type;
}
Corresponding to the use of aliases is:
type Box<Type> = {
contents: Type;
};
Type aliases are different from interfaces. They can describe more than object types, so we can also use type aliases to write some other types of generic helper types.
type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNull<Type> = OneOrMany<Type> | null
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
type OneOrManyOrNullStrings = OneOrMany<string> | null
Array
type (The Array Type)
We have talked about the Array
type before. When we write the type number[]
or string[]
like this, they are actually just Array<number>
and Array<string>
.
function doSomething(value: Array<string>) {
// ...
}
let myArray: string[] = ["hello", "world"];
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));
Similar to the above Box
type, Array
itself is a generic type:
interface Array<Type> {
/**
* Gets or sets the length of the array.
*/
length: number;
/**
* Removes the last element from an array and returns it.
*/
pop(): Type | undefined;
/**
* Appends new elements to an array, and returns the new length of the array.
*/
push(...items: Type[]): number;
// ...
}
Modern JavaScript also provides other generic data structures, such as Map<K, V>
, Set<T>
and Promise<T>
. Because Map
, Set
, Promise
, they can be used with any type.
ReadonlyArray
type (The ReadonlyArray Type)
ReadonlyArray
is a special type, it can describe the array can not be changed.
function doStuff(values: ReadonlyArray<string>) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
// Property 'push' does not exist on type 'readonly string[]'.
}
ReadonlyArray
mainly used to make a statement of intent. When we see a function returning ReadonlyArray
, we are telling us that we can’t change the content. When we see a function that supports the incoming ReadonlyArray
, this is telling us that we can safely pass the array to the function without worrying. Will change the contents of the array.
Unlike Array
, ReadonlyArray
not a constructor function we can use.
new ReadonlyArray("red", "green", "blue");
// 'ReadonlyArray' only refers to a type, but is being used as a value here.
However, we can directly assign a regular array to ReadonlyArray
.
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
TypeScript also for ReadonlyArray<Type>
provides a shorter wording readonly Type[]
.
function doStuff(values: readonly string[]) {
// We can read from 'values'...
const copy = values.slice();
console.log(`The first value is ${values[0]}`);
// ...but we can't mutate 'values'.
values.push("hello!");
// Property 'push' does not exist on type 'readonly string[]'.
}
One last thing to note is that Arrays
and ReadonlyArray
cannot be assigned in both directions:
let x: readonly string[] = [];
let y: string[] = [];
x = y; // ok
y = x; // The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
Tuple Types
The tuple type is another type of Array
. When you know exactly how many elements the array contains and the type of each position element is clearly known, the tuple type is suitable.
type StringNumberPair = [string, number];
In this example, StringNumberPair
is the tuple type of string
and number
Like ReadonlyArray
, it has no effect at runtime, but it makes sense for TypeScript. Because for the type system, StringNumberPair
describes an array, the type of the value at index 0 is string
, and the type of the value at index 1 is number
.
function doSomething(pair: [string, number]) {
const a = pair[0];
const a: string
const b = pair[1];
const b: number
// ...
}
doSomething(["hello", 42]);
If you want to get elements other than the number of elements, TypeScript will prompt an error:
function doSomething(pair: [string, number]) {
// ...
const c = pair[2];
// Tuple type '[string, number]' of length '2' has no element at index '2'.
}
We can also use JavaScript's array destructuring syntax to deconstruct tuples:
function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;
console.log(inputString); // const inputString: string
console.log(hash); // const hash: number
}
The tuple type is useful in APIs that rely heavily on conventions, because it makes the meaning of each element obvious. When we deconstruct, tuples give us degrees of freedom in naming variables. In the above example, we can name the elements
0
and1
as we want.However, not every user thinks this way, so sometimes it may be a better way to use an object with a name describing the attribute.
Except for the length check, the simple tuple type is the same as Array
length
attribute and the specific index attribute.
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
}
In the tuple type, you can also write an optional attribute, but the optional element must be at the end, and it will also affect the type's length
.
type Either2dOr3d = [number, number, number?];
function setCoordinate(coord: Either2dOr3d) {
const [x, y, z] = coord;
const z: number | undefined
console.log(`Provided coordinates had ${coord.length} dimensions`);
// (property) length: 2 | 3
}
Tuples can also use remaining element syntax, but must be of array/tuple type:
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
A tuple with remaining elements will not set length
, because it only knows the known element information at different positions:
const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];
console.log(a.length); // (property) length: number
type StringNumberPair = [string, number];
const d: StringNumberPair = ['1', 1];
console.log(d.length); // (property) length: 2
The existence of optional elements and remaining elements allows TypeScript to use tuples in the parameter list, like this:
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}
Basically equivalent to:
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}
readonly
tuple type (readonly Tuple Types)
The tuple type can also be set to readonly
:
function doSomething(pair: readonly [string, number]) {
// ...
}
In this way, TypeScript will not allow to write any attributes of the readonly
function doSomething(pair: readonly [string, number]) {
pair[0] = "hello!";
// Cannot assign to '0' because it is a read-only property.
}
In most of the code, tuples are only created and will not be modified after use, so it is a good habit to readonly
If we assert an array literal const
, it will also be inferred to readonly
tuple type of 061d45ccc393bd.
let point = [3, 4] as const;
function distanceFromOrigin([x, y]: [number, number]) {
return Math.sqrt(x ** 2 + y ** 2);
}
distanceFromOrigin(point);
// Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'.
// The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.
Although distanceFromOrigin
does not change the elements passed in, the function expects to pass in a variable tuple. Because point
type is inferred to readonly [3, 4]
, it now [number number]
not compatible, so TypeScript gave a error.
TypeScript series
The TypeScript series of articles consists of three parts: official document translation, important and difficult analysis, and practical skills. It covers entry, advanced, and actual combat. It aims to provide you with a systematic learning TS tutorial. The entire series is expected to be about 40 articles. Click here to browse the full series of articles, and suggest to bookmark the site by the way.
WeChat: "mqyqingfeng", add me to the only reader group in Kongyu.
If there are mistakes or not rigorous, please correct me, thank you very much. If you like or have some inspiration, star is welcome, which is also an encouragement to the author.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。