preface

The official TypeScript documentation has been updated for a long time, but the Chinese documentation I could find was still in older versions. Therefore, some new and revised chapters are translated and sorted out.

This article is adapted from the “Object Types” chapter in the TypeScript Handbook.

This paper does not strictly follow the translation of the original text, and some of the content has also been explained and supplemented.

Object Types

In JavaScript, the most basic way to group and distribute data is through objects. In TypeScript, we describe objects by object types.

Object types can be anonymous:

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

It can also be defined using an interface:

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

Or through a type alias:

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

Property Modifiers

Each property in an object type can say something about its type, whether the property is optional, whether the property is read-only, and so on.

Optional Properties

We can add one after the property name, right? The tag indicates 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 });
Copy the code

In this example, xPos and yPos are optional attributes. All of the above calls are legal because they are optional.

We could also try to read these properties, but if we were in strictNullChecks mode, TypeScript would tell us that the property value might be undefined.

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

In JavaScript, if a property value is not set, we get undefined. So we can do something special for 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

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

Here we use deconstruction syntax and provide default values for xPos and yPos. Now the values of xPos and yPos must exist inside paintShape, but they are optional for the caller of paintShape.

Note that there is no way to place type annotations in the deconstruction syntax. This is because in JavaScript, the following syntax represents a completely different meaning.

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 object deconstruction syntax, shape: Shape refers to assigning the value of shape to the local variable shape. XPos: number is also used to create a variable named number based on xPos.

readonlyProperties (readonly Properties)

In TypeScript, properties can be marked as readonly and this does not change any runtime behavior, but an attribute marked as readonly cannot be written during type checking.

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

However, using readonly does not mean that a value is completely immutable, or that the internal contents are immutable. Readonly simply indicates that the property 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}; }Copy the code

TypeScript checks compatibility between two types without considering whether the properties in the two types are readonly, which means that readonly values can be aliased.

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

Index Signatures

Sometimes you don’t know the names of all the attributes in a type in advance, but you do know the characteristics of the values.

In this case, you can use an index signature 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
Copy the code

So we have a StringArray interface with an index signature that says that when a value of type StringArray is indexed by a value of type number, it returns a value of type String.

The attribute type of an index signature must be string or number.

Although TypeScript supports both string and number types, numeric indexes must return subtypes of character indexes. This is because when a number is indexed, 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;
}
Copy the code

Although string indexes are useful for describing dictionary patterns, they also force all attributes to match the return type of the index signature. This is because a string index declaration similar to obj. Property is the same as obj[“property”]. In the following example, the type of name does not match the type of the string index, so the type checker gives 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'.
}
Copy the code

However, if an index signature is a union of attribute types, then attributes of various types are acceptable:

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

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

You cannot set the value of myArray[2] because the index signature is readonly.

Property inheritance (Extending Types)

Sometimes we need a more specific type than the others. As an example, suppose we have a BasicAddress type that describes the fields needed to mail letters and packages in the United States.

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

This is sufficient in some cases, but buildings at the same address often have different unit numbers. We can write AddressWithUnit again:

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

This is fine, but in order to add a field, you have to copy it completely.

We can implement this by inheriting BasicAddress:

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

Using the extends keyword for an interface allows us to effectively copy members from other declared types and add new members at will.

Interfaces can also inherit 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

Intersection Types

TypeScript also provides methods called Intersection Types that merge existing object types.

The & operator is used to define an intersection type:

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

Here, we link Colorful and Circle 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}`);
}
 
// 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'?

Copy the code

Interface inheritance vs. Intersections (Interfalces vs. Intersections)

The two approaches look similar in terms of merge types, but are actually quite different. The most fundamental difference is how the conflict is handled, which is the main reason you decide to go that way.

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

Using inheritance, overwriting a type causes a compilation error, but crossing a type does not:

interface Colorful {
  color: string;
}

type ColorfulSub = Colorful & {
  color: number
}
Copy the code

So what’s the type of the color property, even though it doesn’t give an error? The answer is never, and it gets the intersection of string and number.

Generic Object Types

Let’s write a Box type that can contain any value:

interface Box {
  contents: any;
}
Copy the code

The content property is now of type any, which works, but is prone to rollover.

We can also use unknown instead, but that also means that if we already know the type of contents, we need to do some preventive 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());
Copy the code

A safer approach is to split Box more specifically by contents type:

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

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

It’s just too tedious to write.

So we can create a generic Box that declares a type parameter:

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

You can think of it this way: The Type of Box is the Type that contents has.

When we refer to Box, we need to replace Type with a Type argument:

let box: Box<string>;
Copy the code

Think of Box as a template for an actual Type, and Type is a placeholder that can be substituted for a specific Type. When TypeScript sees Box

, it replaces Box

with Type string, resulting in {contents: string}. In other words, Box

is the same as StringBox.



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

But now that 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>;
Copy the code

This also means that we can use generic functions to avoid function overloading.

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

Type aliases can also use generics. Such as:

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

Using an alias corresponds to:

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

Unlike interfaces, type aliases can describe more than just object types, so we can also use type aliases to write some other kinds 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
Copy the code

ArrayType (The Array Type)

We’ve talked about Array types before, and when we write the type number[] or string[] like this, they’re just shorthand for Array

and Array

.

function doSomething(value: Array<string>) {
  // ...
}
 
let myArray: string[] = ["hello"."world"];
 
// either of these work!
doSomething(myArray);
doSomething(new Array("hello"."world"));
Copy the code

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

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

, Set

, and Promise

. Because of how maps, sets, and Promises behave, they can be used with any type.


,>

ReadonlyArrayType (The ReadonlyArray Type)

ReadonlyArray is a special type that says the array cannot 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[]'.
}
Copy the code

ReadonlyArray is mainly used to declare intent. When we see a function that returns ReadonlyArray, we are telling us that we cannot change its contents. When we see a function that supports passing ReadonlyArray, we are telling us that we can pass arrays into the function without worrying about changing the contents of the array. Unlike Array, ReadonlyArray is 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.
Copy the code

However, we can simply assign a regular array to ReadonlyArray.

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

TypeScript also provides a shorter version of ReadonlyArray

: 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[]'.
}
Copy the code

One last thing to note is that Arrays and ReadonlyArray don’t assign bidirectionally:

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[]'.

Copy the code

Tuple Types

The tuple type is another Array type, and is useful when you know exactly how many elements an Array contains and the type of each positional element.

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

In this example, StringNumberPair is a tuple type of string and number.

Like ReadonlyArray, it doesn’t matter at runtime, but makes sense for TypeScript. Because for the type system, StringNumberPair describes an array where the value at index 0 is of type string and the value at index 1 is of type number.

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

TypeScript prompts an error to retrieve elements other than the number of elements:

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 deconstruct tuples using JavaScript’s array destruct syntax:

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

The tuple type is useful in apis that rely heavily on convention because it makes the meaning of each element obvious. Tuples give us the freedom to name variables when we deconstruct them. In the example above, we can name elements 0 and 1 whatever we want.

However, not everyone thinks this way, so sometimes it might be better to use an object with a name that describes the property.

Except for length checking, the simple tuple type is the same as an Array that declares a length attribute and a 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>;
}
Copy the code

In tuple types, you can also write an optional attribute, but the optional element must come last and also affects 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 the residual element syntax, but must be of type array/tuple:

type StringNumberBooleans = [string.number. boolean[]];type StringBooleansNumber = [string. boolean[],number];
type BooleansStringNumber = [...boolean[], string.number];
Copy the code

Tuples with remaining elements do not set length because it only knows information about known elements in 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

Copy the code

The existence of optional and residual elements allows TypeScript to use tuples in argument lists, like this:

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

Basically equivalent to:

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

readonlyReadonly Tuple Types

Tuple types can also be readonly:

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

This way TypeScript won’t allow you to write any properties of a readonly tuple:

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 are created and never modified after use, so it’s a good practice to set tuples to readonly whenever possible.

If we give an array literal const assertion, it is also inferred to be of readonly tuple type.

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

Although the distanceFromOrigin does not change the element passed in, the function wants to pass in a mutable tuple. TypeScript gives an error because point is inferred to be of type readonly [3, 4], which is incompatible with [number number].

The TypeScript series

TypeScript: github.com/mqyqingfeng…

If you are confused about TypeScript or want to know more about TypeScript, please contact me on wechat: “MQyqingfeng”, public id: “Yui’S JavaScript blog” or “Yayujs”.

If there is any mistake or not precise place, please be sure to give correction, thank you very much. If you like or are inspired by it, welcome star and encourage the author.