• Description : There is currently no Chinese translation of the latest official documents of TypeScript on the Internet, so there is such a translation plan. Because I am also a beginner in TypeScript, I cannot guarantee that the translation will be 100% accurate. If there are errors, please point out in the comment section;
  • translation content : The tentative translation content is TypeScript Handbook , and other parts of the translation document will be added later;
  • project address : TypeScript-Doc-Zh , if it helps you, you can click a star~

The official document address of this chapter: 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 you saw before, object types can be anonymous:

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

Or you can use an interface to name:

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

Or use a type alias to name:

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

In the above example, the object accepted by the function we wrote contains the name attribute (the type must be string ) and the age attribute (the type must be number ).

Attribute modifier

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

Optional attributes

Most of the time, we will find that the object we deal with may have a set of attributes. At this time, we can add the ? symbol after the names of these attributes to mark them as optional attributes.

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. We can choose to provide these two attributes or not, so the above paintShape calls are all valid. What optionality really wants to express is that if this attribute is set, then it's better to have a specific type.

These properties are also accessible-but if strictNullChecks is turned on, TypeScript will prompt us that these properties 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, even if a property has never been set, we can still access it-the value is undefined . We can do special treatment for the situation 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
  // ...
}

Note that this mode of setting default values for unspecified values is very common, so JavaScript provides syntax 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
  // ...
}

Here we paintShape parameters of the deconstruction mode , but also for xPos and yPos provide defaults . Now, xPos and yPos paintShape , and these two parameters are still optional when calling the function.

Note that there is currently no way to use type annotations in destructuring mode. 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'.
}

In an object deconstruction mode, shape: Shape means "capture the shape attribute and redefine it as a local variable Shape Similarly, xPos: number will also create a number , and its value is the value of xPos in the parameter.

Use the mapping modifier to remove optional attributes.

Read-only attribute

In TypeScript, we can mark the attribute as readonly to indicate that this is a read-only attribute. Although this does not change any behavior at readonly , the attributes marked 061a1ec9865eb2 can no longer be overwritten during type checking.

interface SomeType {
  readonly prop: string;
}
 
function doSomething(obj: SomeType) {
  // 可以读取 obj.prop
  console.log(`prop has the value '${obj.prop}'.`);
 
  // 但无法重新给它赋值
  obj.prop = "hello";
// Cannot assign to 'prop' because it is a read-only property.
}

Using the readonly modifier does not necessarily mean that a value is completely unmodifiable - or in other words, it does not mean that its content is unmodifiable. readonly only means that the attribute itself cannot be overwritten.

interface Home {
  readonly resident: { name: string; age: number };
}
 
function visitForBirthday(home: Home) {
  // 我们可以读取并更新 home.resident 属性
  console.log(`Happy birthday ${home.resident.name}!`);
  home.resident.age++;
}
 
function evict(home: Home) {
  // 但我们无法重写 Home 类型的 resident 属性本身
  home.resident = {
       ^^^^^^^^
// Cannot assign to 'resident' because it is a read-only property.
    name: "Victor the Evictor",
    age: 42,
  };
}

readonly is very important to understand the meaning of 061a1ec9865f13. In the process of developing with TypeScript, it can effectively indicate how an object should be used. When TypeScript checks whether two types are compatible, it does not consider whether their properties are read-only, so read-only properties can also 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,
};
 
// 可以正常执行
let readonlyPerson: ReadonlyPerson = writablePerson;
 
console.log(readonlyPerson.age); // 打印 42
writablePerson.age++;
console.log(readonlyPerson.age); // 打印 43

Use the mapping modifier to remove the read-only attribute.

Index signature

Sometimes you cannot know the names of all the attributes of a certain type in advance, but you know the types of these attribute values. In this case, you can use index signatures to describe the types of possible values. for example:

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

In the above code, the StringArray interface has an index signature. This index signature indicates that when StringArray is number by a value of type 061a1ec9865f93, it will return a value of type string

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

Of course, it can also support two types at the same time...

But the premise is that the type returned by the numeric index must be a subtype of the type returned by the string index. This is because, when indexing object properties with numeric values, JavaScript actually converts the numeric value to a string first. This means that using 100 (number) for indexing and "100" (string) for indexing have the same effect, 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;
}

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

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // length 是数字,可以
  name: string; // name 是字符串,可以
}

Finally, you can set the index signature to be read-only, which prevents the attributes 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.

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

Expansion type

It is a very common requirement to expand a more specific type based on a certain type. For example, we have a BasicAddress type used to describe the address information needed to mail a letter or package.

interface BasicAddress {
    name?: string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
}

Normally, this information is sufficient. However, if there are many units in a building at a certain address, then the address information usually needs to have a unit number. At this time, we can use a AddressWithUnit to describe the address information:

interface AddressWithUnit {
    name?: string;
    unit: string;
    street: string;
    city: string;
    country: string;
    postalCode: string;
}

This is of course no problem, but the disadvantage is that although only a single domain is added, we have to rewrite all domains in BasicAddress Then we might as well use a method, we expand the original BasicAddress type, and add a new domain unique AddressWithUnit

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

extends keyword following an interface allows us to efficiently copy members from other named types and add any new members we want. This has a great effect on reducing the type declaration statements we have to write, and at the same time it can show that there is a connection between several different type declarations with the same attributes. For example, AddressWithUnit does not need to rewrite the street attribute, and since the street attribute comes from BasicAddress , the developer can know that there is a certain connection between the two types.

The interface 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,
};

Cross type

The interface allows us to build new types by extending the original types. TypeScript also provides another structure called "cross-type" that can be used to combine existing object types.

An intersection type can be defined through the &

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

Here, we combine the Colorful and Circle types to produce a new type, which has all members of Colorful and Circle

function draw(circle: Colorful & Circle) {
  console.log(`Color was ${circle.color}`);
  console.log(`Radius was ${circle.radius}`);
}
 
// 可以运行
draw({ color: "blue", radius: 42 });
 
// 不能运行
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 VS cross type

At present, we have learned that there are two ways to combine two similar but different types. Using the interface, we can extends clause; using the cross type, we can also achieve a similar effect, and use the type alias to name the new type. The essential difference between the two lies in the way they handle conflicts, and this difference is usually the main reason why we choose between interface and cross-type type aliases.

Generic object type

Suppose we have a Box , which may contain any type of value: string , number , Giraffe etc.

interface Box {
    contents: any;
}

Now, the type of the contents any , which is of course no problem, but using any may cause type safety issues.

So we can use unknown instead. But this means that as long as we know the contents , we need to do a preventive check or use an error-prone type assertion.

interface Box {
  contents: unknown;
}
 
let x: Box = {
  contents: "hello world",
};
 
// 我们可以检查 x.contents
if (typeof x.contents === "string") {
  console.log(x.contents.toLowerCase());
}
 
// 或者使用类型断言
console.log((x.contents as string).toLowerCase());

Another way to ensure type safety is to create a different Box type contents

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

But this means that we need to create different functions, or function overloads, so that we can operate different types of Box .

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 brings a lot of boilerplate code. Moreover, we may also introduce new types and overloads in the future, which is a bit redundant. After all, our Box type and overload are only different types, which are essentially the same.

It might as well use a way, that is, let the Box type declare a type parameter and use generics.

interface Box<Type> {
    contents: Type;
}

You can interpret this code as " Box type is Type , it contents type is Type ." Next, when we quote Box , we need to pass a type parameter to replace Type .

let box: Box<string>;

If you regard Box as the actual type of template, then Type is a placeholder that will be replaced by other types. When TypeScript see Box<string> time, it will Box<Type> all Type replaced string , to obtain a similar { contents: string } object. In other words, Box<string> and the previous example StringBox are equivalent.

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

Because Type can be replaced by any type, Box is reusable. This means that when our contents needs a new type, there is no need to declare a new Box (although there is no problem in doing so).

interface Box<Type> {
    contents: Type;
}
interface Apple {
    //...
}
// 和 { contents: Apple } 一样
type AppleBox = Box<Apple>;

This also means that by using the generic function , we can completely avoid overloading.

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

It is worth noting that type aliases can also use generics. The previously defined Box<Type> interface:

interface Box<Type> {
    contents: Type;
}

Can be rewritten as the following type alias:

type Box<Type> = {
    contents: Type;
};

Type aliases are not the same as interfaces. They can not only be used to describe object types. So we can also use type aliases to write other generic tool 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         

We will come back to explain type aliases later.

Array type

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

In fact, in this manual, we have been dealing with a generic type, which is the Array (array) type. number[] type or string[] type that we write are actually abbreviations of Array<number> and Array<string>

function doSomething(value: Array<string>) {
  // ...
}
 
let myArray: string[] = ["hello", "world"];
 
// 下面两种写法都可以!
doSomething(myArray);
doSomething(new Array("hello", "world"));

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

interface Array<Type> {
  /**
   * 获取或者设置数组的长度
   */
  length: number;
 
  /**
   * 移除数组最后一个元素,并返回该元素
   */
  pop(): Type | undefined;
 
  /**
   * 向数组添加新元素,并返回数组的新长度
   */
  push(...items: Type[]): number;
 
  // ...
}

Modern JavaScript also provides other data structures that are also generic, such as Map<K,V> , Set<T> and Promise<T> . This actually means that the Map , Set and Promise enable them to handle any set of types.

Read-only array type

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

function doStuff(values: ReadonlyArray<string>) {
  // 我们可以读取 values 
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // ...但是无法修改 values
  values.push("hello!");
         ^^^^
        // Property 'push' does not exist on type 'readonly string[]'.
}

Like the readonly modifier of an attribute, it is mainly a tool used to indicate intent. When we see a function returning ReadonlyArray , it means that we do not intend to modify the array; when we see a function accepting ReadonlyArray as a parameter, it means that we can pass any array to this function without worrying about the array being Revise.

Array , ReadonlyArray is no corresponding constructor for 061a1ec98664f0.

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

However, we can assign the ordinary Array to ReadonlyArray .

const roArray: ReadonlyArray<string> = ["red","green","blue"];

TypeScript not only Array<Type> provides a shorthand Type[] , also ReadonlyArray<Type> provides a shorthand readonly Type[] .

function doStuff(values: readonly string[]) {
  // 我们可以读取 values
  const copy = values.slice();
  console.log(`The first value is ${values[0]}`);
 
  // ...但无法修改 values
  values.push("hello!");
        ^^^^^
      // Property 'push' does not exist on type 'readonly string[]'.
}

The last thing to note is that, readonly attribute modifier of Array , the ReadonlyArray between the ordinary 061a1ec9866569 and 061a1ec986656a 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[]'.

Tuple type

The tuple type is a special Array . The number of its elements and the type corresponding to each element are clear.

type StringNumberPair = [string, number];

Here, StringNumberPair is a string type containing 061a1ec98665d7 type and number type. Like ReadonlyArray , it has no corresponding runtime representation, but it is still very important for TypeScript. For the type system, StringNumberPair describes an array: the position with the 0 string type 1 , and the position with the number type 061a1ec98665df.

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

If the subscript is out of bounds when accessing the tuple element, an error will be thrown.

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 an array of deconstruction JavaScript to deconstruction tuple .

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

Tuple types are useful in highly convention-based APIs because the meaning of each element is "unambiguous." This gives us a kind of flexibility, allowing us to give arbitrary names to variables when deconstructing tuples. In the above example, we can give any name to the elements with subscripts 0 and 1.

But what is "clear"? Every developer has different opinions. Maybe you need to reconsider whether it is better to use objects with descriptive properties in the API.

In addition to length checking, a simple tuple type like this is actually equivalent to an object that declares the properties of a specific subscript and contains the length properties of the numeric literal type.

interface StringNumberPair {
  // 特定的属性
  length: 2;
  0: string;
  1: number;
 
  // 其它 Array<string | number> 类型的成员
  slice(start?: number, end?: number): Array<string | number>;
}

Another thing you might be interested in is that tuple types can also have optional elements, just add ? after an element type. Optional tuple elements can only appear at the end, and will 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
}

Tuples can also use the spread operator, and the operator must be followed by an array or tuple.

type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
  • StringNumberBooleans represents such a tuple: its first two elements are string and number respectively, and it is followed by several boolean type.
  • StringBooleansNumber represents a tuple: its first element is string , followed by several boolean , and the last element is number .
  • BooleansStringNumber represents such a tuple: there are several boolean front of it, and the last two elements are type string and number

There is no clear length for the tuple using the spread operator-it is clear that there are elements of the corresponding type in different positions.

const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];

Why are optional elements and expansion operators useful? Because it allows TypeScript to map parameter lists to tuples. can be used in the remaining parameters of 161a1ec98667ea and the expansion operator , so the following code:

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

It is equivalent to this code:

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

Sometimes, we use the remaining parameters to accept a variable number of parameters, and at the same time require that the parameters are not less than a certain number, and do not want to introduce intermediate variables for this purpose, this way of writing above is very convenient at this time.

Read-only tuple type

There is one last thing to note about the tuple type, that is-the tuple type can also be read-only. By adding the readonly modifier in front of the tuple, we can declare a read-only tuple type-just Like shorthand for read-only array.

function doSomething(pair: readonly [string, number]) {
  // ...
}

It is not possible to override any attributes of read-only tuples in TypeScript.

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, the tuple does not need to be modified after it is created, so it is a good default to annotate the tuple as a read-only type. It is also very important const will be inferred as a read-only tuple.

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

Here, distanceFromOrigin does not modify the elements of the tuple, but it expects to accept a mutable tuple. Because point type is inferred to readonly [3,4] , so it [number, number] are incompatible, because the latter can not guarantee point elements will not be modified.


Chor
2k 声望5.9k 粉丝