• Note: There are currently no Chinese translations of the latest TypeScript official documentation available online, so there is a translation plan. Since I’m also a beginner to TypeScript, I can’t guarantee 100% accuracy in TypeScript translations, so I feel free to point out any errors in the comments section.
  • Translation content: The temporary translation content is TypeScript Handbook, and other parts of the translation document will be supplemented later.
  • Project address: typescript-doc-zh, if it helps you, you can click a star ~

The official document address of this chapter is Object Types

Object type

In JavaScript, the most basic way to group and pass data is to use objects. In TypeScript, we are represented by object types.

As we saw earlier, object types can be anonymous:

function greet(person: { name: string; age: number }) {
  return "Hello " + person.name;
}
Copy the code

Alternatively, you can use an interface name:

interface Person {
  name: string;
  age: number;
}
 
function greet(person: Person) {
  return "Hello " + person.name;
}
Copy the code

Or use a type alias to name it:

type Person = {
  name: string;
  age: number;
};
 
function greet(person: Person) {
  return "Hello " + person.name;
}
Copy the code

In the example above, we write a function that takes an object containing the name attribute (type must be string) and the age attribute (type must be number).

Attribute modifier

Each property in an object type can specify something: the property type, whether the property is optional, and whether the property is writable.

Optional attribute

Most of the time, we find ourselves dealing with objects that may have a set of attributes. At this point, we can add? Symbol to mark them as optional properties.

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 });
Copy the code

In this example, both xPos and yPos are optional attributes. We can optionally provide or not provide these two properties, so the paintShape call above is valid. What optionality really means is that if this property is set, it better have a specific type.

These properties are also accessible — but if strictNullChecks is enabled, TypeScript tells us that these properties might be undefined.

function paintShape(opts: PaintOptions) {
  letxPos = opts.xPos; ^ ^ ^ ^// (property) PaintOptions.xPos? : number | undefined
  letyPos = opts.yPos; ^ ^ ^ ^// (property) PaintOptions.yPos? : number | undefined
  // ...
}
Copy the code

In JavaScript, we can access a property even if it has never been set — the value is undefined. We can do something special with 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
  // ...
}
Copy the code

Note that this pattern of setting default values for values not specified is common, so JavaScript provides syntax-level support.

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
  // ...
}
Copy the code

Here we use deconstruction mode for the parameters of paintShape, and also provide default values for xPos and yPos. Now, xPos and yPos must have values in the body of paintShape, and they are still optional when the function is called.

Note that there is currently no way to use type annotations in destructuring patterns. This is because the following syntax has other semantics in JavaScript

function draw({ shape: Shape, xPos: number = 100 / *... * / }) { render(shape); ^ ^ ^ ^ ^ ^// Cannot find name 'shape'. Did you mean 'Shape'?render(xPos); ^ ^ ^ ^ ^// Cannot find name 'xPos'.
}
Copy the code

In an object deconstruction mode, shape: Shape means “to capture the shape property and redefine it as a local variable named shape”. Similarly, xPos: number creates a variable named number whose value is the xPos value in the argument.

Optional attributes can be removed using mapping modifiers.

Read-only property

In TypeScript, we can mark an attribute as readonly to indicate that it is read-only. While this does not change any behavior at run time, properties marked readonly cannot be overridden during type checking.

interface SomeType {
  readonly prop: string;
}
 
function doSomething(obj: SomeType) {
  // Obj.prop can be read
  console.log(`prop has the value '${obj.prop}'. `);
 
  // It cannot be reassigned
  obj.prop = "hello";
// Cannot assign to 'prop' because it is a read-only property.
}
Copy the code

Using the readonly modifier does not necessarily mean that a value is completely immutable — or, in other words, that its contents are immutable. Readonly only indicates that the property itself cannot be overridden.

interface Home {
  readonly resident: { name: string; age: number };
}
 
function visitForBirthday(home: Home) {
  // We can read and update the home.resident property
  console.log(`Happy birthday ${home.resident.name}! `);
  home.resident.age++;
}
 
function evict(home: Home) {
  But we cannot override the resident property itself of type Home
  home.resident = {
       ^^^^^^^^
// Cannot assign to 'resident' because it is a read-only property.
    name: "Victor the Evictor".age: 42}; }Copy the code

It is important to understand what readOnly means. In development with TypeScript, it effectively indicates how an object should be used. TypeScript does not consider whether properties of two types are read-only when checking for compatibility, so read-only properties can also be changed through aliases.

interface Person {
  name: string;
  age: number;
}
 
interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}
 
let writablePerson: Person = {
  name: "Person McPersonface".age: 42};// It can be executed normally
let readonlyPerson: ReadonlyPerson = writablePerson;
 
console.log(readonlyPerson.age); / / print 42
writablePerson.age++;
console.log(readonlyPerson.age); / / print 43
Copy the code

You can remove read-only attributes using mapping modifiers.

The index sign

Sometimes you don’t know the names of all the properties of a type in advance, but you do know the types of those property values. In this case, you can use index signatures to describe the types of possible values. Here’s an example:

interface StringArray {
    [index: number] :string
}
const myArray: StringArray = getStringArray();
const secondItem = myArray[1]; ^ ^ ^ ^ ^ ^ ^ ^ ^ ^// const secondItem: string
Copy the code

In the code above, the StringArray interface has an index signature. The index signature indicates that when StringArray is indexed by a value of type number, it will return a value of type String.

The attribute type of an index signature is either String or number.

Of course, you can also support both types…

However, the type returned by a numeric index must be a subtype of the type returned by a string index. This is because when using numeric index object attributes, JavaScript actually converts the value to a string first. This means that indexing with 100 (a numeric value) is the same as indexing with “100” (a string), so the two must be consistent.

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;
}
Copy the code

However, if the type described by the index signature is itself a combination of various attribute types, then different types of attributes are allowed:

interface NumberOrStringDictionary {
  [index: string] :number | string;
  length: number; // length is a number, ok
  name: string; // Name is a string, ok
}
Copy the code

Finally, you can set the index signature to be read-only to prevent the properties of the corresponding index from being reassigned:

interface ReadonlyStringArray {
  readonly [index: number] :string;
}
 
let myArray: ReadonlyStringArray = getReadOnlyStringArray();
myArray[2] = "Mallory";
// Index signature in type 'ReadonlyStringArray' only permits reading.
Copy the code

Because the index signature is set to read-only, the value of myArray[2] cannot be changed.

Expanding type

It is a common requirement to extend a more specific type based on a type. For example, we have a BasicAddress type that describes the address information needed to mail a letter or package.

interfaceBasicAddress { name? :string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
}
Copy the code

In general, this information is sufficient, but if a building at an address has many units, the address information is usually required to have a unit number. In this case, we can use an AddressWithUnit to describe address information:

interfaceAddressWithUnit { name? :string;
    unit: string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
}
Copy the code

This is fine, but the downside is that even though we’re simply adding one field, we have to write all the fields in BasicAddress over and over again. Instead, we extend the original BasicAddress type and add a new field unique to AddressWithUnit.

interfaceBasicAddress { name? :string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
 
interface AddressWithUnit extends BasicAddress {
  unit: string;
}
Copy the code

The extends keyword following an interface allows us to efficiently copy members from other named types and add any new members we want. This is a great way to reduce the number of type declarations we have to write, and also to show that there are relationships between several different type declarations that have the same attributes. For example, AddressWithUnit does not need to write the street attribute repeatedly, and since the street attribute comes from BasicAddress, the developer knows that there is a connection between the two types.

Interfaces can also be extended from multiple types:

interface Colorful {
  color: string;
}
 
interface Circle {
  radius: number;
}
 
interface ColorfulCircle extends Colorful, Circle {}
 
const cc: ColorfulCircle = {
  color: "red".radius: 42};Copy the code

Cross type

Interfaces allow us to build new types by extending existing types. TypeScript also provides another construct called “cross types” that can be used to combine existing object types.

An intersection type can be defined with the & operator:

interface Colorful {
    color: string;
}
interface Circle {
    radius: number;
}
type ColorfulCircle = Colorful & Circle;
Copy the code

Here, we combine the Colorful and Circle types to create a new type that has all the members of Colorful and Circle.

function draw(circle: Colorful & Circle) {
  console.log(`Color was ${circle.color}`);
  console.log(`Radius was ${circle.radius}`);
}
 
// It works
draw({ color: "blue".radius: 42 });
 
// Cannot run
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'? * / 
Copy the code

Interface VS cross type

So far, we’ve seen two ways to combine similar but different genres. Using an interface, we can extend an existing type with the extends clause; We can achieve a similar effect with crossed types and use type aliases to name new types. The essential difference between the two is how they handle conflicts, and this difference is often the main reason we choose between an interface and a type alias for an intersecting type.

Generic object types

Suppose we have a Box type that can contain any type of value: string, number, Giraffe, and so on.

interface Box {
    contents: any;
}
Copy the code

Now, the contents property is of type any, which is fine, but using any can cause type-safety problems.

So we can use unknown instead. But that means that as soon as we know the type of contents, we need to do a preventive check or use error-prone type assertions.

interface Box {
  contents: unknown;
}
 
let x: Box = {
  contents: "hello world"};// We can check x.contents
if (typeof x.contents === "string") {
  console.log(x.contents.toLowerCase());
}
 
// Or use type assertion
console.log((x.contents as string).toLowerCase());
Copy the code

Another way to ensure type safety is to create a different Box type for each different type of contents.

interface NumberBox {
  contents: number;
}
 
interface StringBox {
  contents: string;
}
 
interface BooleanBox {
  contents: boolean;
}
Copy the code

But that means we need to create different functions, or overloads of functions, to operate on different Box 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;
}
Copy the code

This leads to a lot of boilerplate code. Also, we may introduce new types and overloads later on, which may be a bit redundant, since our Box types and overloads are essentially the same, but only of different types.

Instead, let the Box type declare a type parameter and use generics.

interface Box<Type> {
    contents: Type;
}
Copy the code

You can read this code as “Box is of Type, its contents is of Type.” Then, when we reference Box, we need to pass a Type argument in place of Type.

let box: Box<string>;
Copy the code

If you think of Box as a template for the actual Type, Type is a placeholder that can be replaced by another Type. When TypeScript sees Box

, it replaces all types in Box

with string, resulting in an object similar to {contents: string}. In other words, Box

is equivalent to the StringBox in the previous example.


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
Copy the code

Because Type can be replaced by any Type, Box is reusable. This means that when our contents needs a new type, there’s no need to declare a new Box type at all (although there’s nothing wrong with that).

interface Box<Type> {
    contents: Type;
}
interface Apple {
    / /...
}
// Same as {contents: Apple}
type AppleBox = Box<Apple>;
Copy the code

This also means that by using generic functions, we can avoid overloading altogether.

function setContents<Type> (box: Box<Type>, newContents: Type) {
    box.contents = newContents;
}
Copy the code

It is worth noting that type aliases can also use generics. The Box

interface defined previously:

interface Box<Type> {
    contents: Type;
}
Copy the code

You can rewrite it to the following type alias:

type Box<Type> = {
    contents: Type;
};
Copy the code

Unlike interfaces, type aliases can be used to describe more than just object types. So we can also use type aliases to write other generic utility types.

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
typeOneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>; ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^//type OneOrManyOrNull<Type> = OneOrMany<Type> | null
type OneOrManyOrNullStrings = OneOrManyOrNull<string>; ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^// type OneOrManyOrNullStrings = OneOrMany<string> | null         
Copy the code

We’ll circle back to type aliases later.

An array type

Generic object types are usually some kind of container type that works independently of the types of the members they contain. Data structures work ideally this way and can be reused even if the data type is different.

In fact, in this manual, we’ve been dealing with one generic type, the Array type. The number[] type we write, or the string[] type we write, is actually a shorthand for Array

and Array

.

function doSomething(value: Array<string>) {
  // ...
}
 
let myArray: string[] = ["hello"."world"];
 
// You can write it either way.
doSomething(myArray);
doSomething(new Array("hello"."world"));
Copy the code

Just like the Box type above, Array itself is a generic:

interface Array<Type> {
  /** * gets or sets the length of the array */
  length: number;
 
  /** * removes the last element of the array and returns that element */
  pop(): Type | undefined;
 
  /** * adds a new element to the array and returns the new length of the array */push(... items: Type[]):number;
 
  // ...
}
Copy the code

Modern JavaScript provides other data structures that are also generic, such as Map

, Set

, and Promise

. What this really means is that maps, sets, and Promises are represented in such a way that they can handle any Set of types.


,v>

Read-only array type

ReadonlyArray (read-only array) is a special type that describes arrays that cannot be modified.

function doStuff(values: ReadonlyArray<string>) {
  // We can read values
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  / /... But you cannot change values
  values.push("hello!"); ^ ^ ^ ^// Property 'push' does not exist on type 'readonly string[]'.
}
Copy the code

Like the Readonly modifier for a property, it is primarily a tool to indicate intent. When we see a function return ReadonlyArray, that means we’re not going to modify the array; When we see a function that takes ReadonlyArray as an argument, that means we can pass any array to the function without worrying about the array being modified.

Unlike Array, ReadonlyArray has no corresponding constructor to use.

new ReadonlyArray("red"."green"."blue"); ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^// 'ReadonlyArray' only refers to a type, but is being used as a value here.
Copy the code

However, we can assign a normal Array to ReadonlyArray.

const roArray: ReadonlyArray<string> = ["red"."green"."blue"];
Copy the code

TypeScript provides the abbreviation Type[] for Array

and ReadonlyArray

for readonly Type[].

function doStuff(values: readonly string[]) {
  // We can read values
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  / /... You cannot modify values
  values.push("hello!"); ^ ^ ^ ^ ^// Property 'push' does not exist on type 'readonly string[]'.
}
Copy the code

One final thing to note is that unlike the readonly attribute modifier, the assignability between normal Array and ReadonlyArray is not bidirectional.

let x: readonly string[] = [];
let y: string[] = [];
 
x = y;
y = x;
^
// The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.
Copy the code

A tuple type

A tuple type is a special Array type where the number of elements and the type of each element are explicit,

type StringNumberPair = [string.number];
Copy the code

Here, StringNumberPair is a tuple type that contains both string and number. Like ReadonlyArray, it has no runtime representation, but is still important to TypeScript. For a type system, StringNumberPair describes an array where the subscript 0 position contains a string value and the subscript 1 position contains a number value.

function doSomething(pair: [string.number]) {
  const a = pair[0];
        ^
     //const a: string
  const b = pair[1];
	    ^        
     // const b: number
  // ...
}
 
doSomething(["hello".42]);
Copy the code

An error is thrown if the subscript is out of bounds when accessing a tuple element.

function doSomething(pair: [string.number]) {
  // ...
 
  const c = pair[2];
    		    ^	
// Tuple type '[string, number]' of length '2' has no element at index '2'.
}
Copy the code

We can also use JavaScript array deconstruction to deconstruct tuples.

function doSomething(stringHash: [string.number]) {
  const [inputString, hash] = stringHash;
 
  console.log(inputString); ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^// const inputString: string
 
  console.log(hash); ^ ^ ^ ^// const hash: number
}
Copy the code

Tuple types are useful in highly convention based apis because the meaning of each element is “unambiguous.” This gives us the flexibility to name variables as we deconstruct tuples. In the example above, we can give any name to elements with subscripts 0 and 1.

But what is “clear”? Every developer has a different perspective. You might want to rethink whether it’s better to use objects with descriptive attributes in your API.

With the exception of length checking, a simple tuple type like this is equivalent to an object that declares a property of a particular subscript and contains the length property of a numeric literal type.

interface StringNumberPair {
  // Specific attributes
  length: 2;
  0: string;
  1: number;
 
  / / other Array < string | number > type of membersslice(start? :number, end? :number) :Array<string | number>;
}
Copy the code

Another thing you might be interested in is that tuple types can also have optional elements, just by adding? . Optional tuple elements can only appear last and affect the length of the type.

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
}
Copy the code

Tuples can also use expansion operators, which must be followed by arrays or tuples.

type StringNumberBooleans = [string.number. boolean[]];type StringBooleansNumber = [string. boolean[],number];
type BooleansStringNumber = [...boolean[], string.number];
Copy the code
  • StringNumberBooleansRepresents a tuple whose first two elements are, respectivelystringnumberType, followed by several at oncebooleanType element.
  • StringBooleansNumberRepresents a tuple whose first element isstringType, followed by severalbooleanType, and the last element isnumber.
  • BooleansStringNumberRepresents a tuple that is preceded by severalbooleanType, and the last two elements are respectivelystringnumberType.

A tuple using the expansion operator has no definite length — all that is clear is that it has elements of the corresponding type at different positions.

const a: StringNumberBooleans = ["hello".1];
const b: StringNumberBooleans = ["beautiful".2.true];
const c: StringNumberBooleans = ["world".3.true.false.true.false.true];
Copy the code

Why are optional elements and expansion operators useful? Because it allows TypeScript to map argument lists to tuples. Tuples can be used in remaining arguments and expansion operators, so the following code:

function readButtonInput(. args: [string.number.boolean[]]) {
  const [name, version, ...input] = args;
  // ...
}
Copy the code

This is equivalent to this code:

function readButtonInput(name: string, version: number. input:boolean[]) {
  // ...
}
Copy the code

This is handy when we want to accept a variable number of arguments with a residual argument, but we want at least a certain number of arguments, and we don’t want to introduce intermediate variables.

Read-only tuple type

One last thing to note about tuple types is that tuple types can also be read-only. By prefacing a tuple with the readonly modifier, we can declare a read-only tuple type — just like the shorthand for read-only arrays.

function doSomething(pair: readonly [string.number]) {
  // ...
}
Copy the code

You cannot override any property of a read-only tuple in TypeScript.

function doSomething(pair: readonly [string.number]) {
  pair[0] = "hello!";
       ^
// Cannot assign to '0' because it is a read-only property.
}
Copy the code

In most code, tuples need not be modified once they are created, so annotating them as read-only is a good default. It is also important to note that array literals that use const assertions will be inferred as read-only tuples.

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]'. */
Copy the code

Here, the distanceFromOrigin does not modify the elements of a tuple, but it expects to accept a mutable tuple. Since point’s type is inferred to be readonly [3,4], it is incompatible with [number, number], which does not guarantee that the elements of point will not be modified.