Learning path · Level 8 · Tutorial 2

Define Reusable Object and Array Types

Describe records and collections with reusable TypeScript contracts, then share those contracts between modules without creating runtime data.

  • Level: Intermediate
  • Duration: 18 min
  • 8 steps
Start tutorial Back to the path
Define Reusable Object and Array Types

What you will go through

  1. Describe a Record with an Object Type Describe required product record properties with an inline TypeScript object type. 2 min
  2. Name and Reuse an Object Shape Replace repeated inline object annotations with one reusable type alias for product records. 2 min
  3. Express an Object Contract with an Interface Define an interface for a simple Product record, compare it with a type alias, and practice their equivalent contract roles. 2 min
  4. Type Collections of Records Annotate primitive arrays and product collections so that every element follows the intended contract. 2 min
  5. Compose Nested and Optional Properties Build Product records with a required nested Category and an optional string description. 3 min
  6. Predict Structural Compatibility Predict assignment compatibility for typed product records, including the extra-property check for fresh object literals. 3 min
  7. Share Contracts Between Modules Share reusable catalog contracts between modules while keeping type-only dependencies separate from runtime data imports. 2 min
  8. Apply and Review the Catalog Contracts Complete and verify a reusable catalog model across contract, data, and entry modules. 3 min

What you will learn

  • Define reusable record contracts with type aliases and interfaces.
  • Describe arrays, nested records, and optional properties using explicit types.
  • Predict whether a record satisfies a structural contract and correct missing or incompatible fields.
  • Export a data contract and consume it in another module with import type.

Before you start

  • Enable Strict Type Checking in a Local Vite Project
  • Represent Records and Lists with Objects and Arrays
  • Split Browser Code into JavaScript Modules

Step 1 of 8

Describe a Record with an Object Type

Describe required product record properties with an inline TypeScript object type.

Read an inline object contract

Read the contract and the record separately

An inline object contract appears after a variable name and colon. Inside its braces, each property signature states a required property name and the value type it must receive.

For this catalog product, id and name must hold strings, while price must hold a number. The annotation describes the required shape; the object literal supplies the actual record.

Map each property to its value type

A property-to-type map connects id to a string value, name to a string value, and price to a number value in a Desk Lamp record.

The contract requires the property names and their matching value types.

Assign a record that satisfies the contract

Required properties must be present and compatible

The annotation after : is the contract checked by TypeScript. The object literal after = is the record being assigned. Because every property in this contract is required, the record must include all three properties with compatible values.

If a required property is missing, add it with a compatible value. If a value has the wrong type, replace it with a value of the declared type.

Type-checks

typescript
const product: {
  id: string;
  name: string;
  price: number;
} = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99
};

Tip

Verify locally

After saving your TypeScript file, run npx tsc --noEmit to perform a type check using your existing strict configuration.

Identify why an assignment fails

Diagnose an incompatible value

Does not type-check:

const product: {
  id: string;
  name: string;
  price: number;
} = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: '29.99'
};

Which statement identifies the assignment problem?

Apply the inline contract

Complete the product contract

Complete the inline product annotation so this Desk Lamp record type-checks.

typescript
const product: {
  id: ;
  name: ;
  price: ;
} = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99
};

Step 2 of 8

Name and Reuse an Object Shape

Replace repeated inline object annotations with one reusable type alias for product records.

Find the duplication

Spot the repeated shape

When the same record shape appears in more than one annotation, the property list becomes repeated maintenance. Both records below require the same three properties with the same value types.

Repeated inline annotations

Two independently created product records repeat one inline contract.

typescript
const product: {
  id: string;
  name: string;
  price: number;
} = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
};

const notebook: {
  id: string;
  name: string;
  price: number;
} = {
  id: 'p-102',
  name: 'Notebook',
  price: 4.5,
};

Reuse one contract

Name the contract

A type declaration gives that object shape one reusable name. Use the name in each annotation instead of copying the inline object type. The values still create two separate records.

One alias, two records

The Product alias is checked at both declarations.

typescript
type Product = {
  id: string;
  name: string;
  price: number;
};

const product: Product = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
};

const notebook: Product = {
  id: 'p-102',
  name: 'Notebook',
  price: 4.5,
};

Reuse diagram

One central product contract shape branches to separate desk lamp and notebook records with matching properties.

One named contract can check several independently created records.

Tip

Shared requirements

A type alias is a compile-time contract, not a product object. Declaring it does not create runtime data. If you later add a required property to the shared contract, every value annotated with that alias is checked against the new requirement during the type check.

Practice: extract and reuse

Refactor the repeated shape

Extract the repeated object shape into one alias and apply that alias to both record declarations. Keep the runtime values unchanged.

typescript
type  = {
  id: ;
  name: ;
  price: ;
};

const product:  = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
};

const notebook:  = {
  id: 'p-102',
  name: 'Notebook',
  price: 4.5,
};

Step 3 of 8

Express an Object Contract with an Interface

Define an interface for a simple Product record, compare it with a type alias, and practice their equivalent contract roles.

Define the interface contract

A named object contract

An interface gives an object contract a name. Its property signatures specify each required property and its value type. In this alternative declaration, Product is the contract; product is the independently created Desk Lamp record.

The annotation const product: Product asks the type checker to compare the record with that contract. Declaring an interface does not create a product object or add runtime validation.

Interface alternative

typescript
interface Product {
  id: string;
  name: string;
  price: number;
}

const product: Product = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
};

Property-to-type map

A diagram maps the required id, name, and price properties in a Product interface to matching values in a Desk Lamp record.

Each required property in the Product interface has a corresponding value of the declared type in the Desk Lamp record.

Compare the declaration forms

Same simple requirements

For this simple record, an interface and an object type alias require the same id, name, and price properties. Both can annotate a value as Product.

They are separate alternatives: do not declare both with the same name in one module. After comparing them, continue using the Product type alias for the catalog contract.

Interface and alias comparison

Comparison of two separate ways to name the same simple Product record contract.

For this Product shape, the checker applies the same required-property rules.
FeatureInterface formType alias form
Named declarationinterface Product { ... }type Product = { ... };
Required propertyid: string;id: string;
Value annotationconst product: Productconst product: Product
Simple Product resultSame required fieldsSame required fields

Separate alternative: type alias

typescript
type Product = {
  id: string;
  name: string;
  price: number;
};

const product: Product = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
};

Practice the interface syntax

Complete the interface

Complete the property signatures so this interface describes the same simple Product contract.

typescript
interface Product {
  id: ;
  name: ;
  price: ;
}

Check the equivalence

Match equivalent parts

Match each interface part to its corresponding role or syntax in the type-alias alternative.

Tap an item and then its matching pair.

Step 4 of 8

Type Collections of Records

Annotate primitive arrays and product collections so that every element follows the intended contract.

Use brackets to describe array elements

Read array element types

An array annotation uses brackets after the element type. string[] means “an array in which every element is a string.”

The annotation belongs to the collection variable, not to one particular position. In the same way, Product[] means every record placed in that collection must satisfy the reusable Product contract.

One element contract, many elements

Diagram comparing a string array with a product-record array, showing one element type applied to every item.

The brackets mean a collection repeats one element contract.

Apply Product to the whole catalog

Start empty, then add records

Use the existing Product alias as the element type. An explicit annotation is useful even when a collection starts empty: it tells the type checker what future elements must look like.

The following catalog starts with products empty, then adds the Desk Lamp record through product and adds the Notebook record directly. This code Type-checks.

Type-checks

A typed collection accepts Product records both at creation time and through later additions.

typescript
type Product = {
  id: string;
  name: string;
  price: number;
};

const product: Product = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
};

const products: Product[] = [];

products.push(product);
products.push({
  id: 'p-102',
  name: 'Notebook',
  price: 4.5,
});

Does not type-check

The same element contract also checks records added later. This does not type-check because the price property is a string, while Product requires a number.

typescript
products.push({
  id: 'p-102',
  name: 'Notebook',
  price: '4.5',
});

Complete the collection annotation

Practice: type an empty collection

Complete the annotation so this empty collection accepts only product records.

typescript
const products:  = [];

products.push({
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
});

Assess typed collection additions

Check a collection and its addition

Which snippet both creates an explicitly typed empty product collection and adds an element that type-checks?

Step 5 of 8

Compose Nested and Optional Properties

Build Product records with a required nested Category and an optional string description.

Require a nested record

Nest a Category record

A property in one contract can use another named contract as its type. Here, every Product must include a category record that satisfies Category. The category property is required because it has no question mark.

Required nested contract

One Product contract can refer to the Category interface.

typescript
interface Category {
  id: string;
  label: string;
}

type Product = {
  id: string;
  name: string;
  price: number;
  category: Category;
  description?: string;
};

Product contains a Category

Diagram showing that Product requires a nested Category record, while description may be omitted.

The category property must contain a record with Category’s required id and label fields.

Supply or omit description

Make one property optional

The question mark in description?: string means a Product record may omit description. It does not create a default description. If a record supplies description, that concrete value must be a string.

Two valid catalog records

The Desk Lamp supplies a description. The Notebook omits it, but both records supply a complete category.

typescript
const product: Product = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
  category: { id: 'home', label: 'Home' },
  description: 'Warm desk light'
};

const products: Product[] = [
  product,
  {
    id: 'p-102',
    name: 'Notebook',
    price: 4.5,
    category: { id: 'office', label: 'Office' }
  }
];

Tip

Check each property’s role

A nested property has its own contract: category needs both id and label with string values. Optional applies only to description—not to category.

Practice the declarations

Complete the contract

Complete the declarations for the nested Category record and the optional Product property.

typescript
interface Category {
  id: ;
  label: string;
}

type Product = {
  id: string;
  name: string;
  price: number;
  category: ;
  description: ;
};

Assess nested and optional fields

Classify Product records

Match each fresh object literal assignment to its type-check result.

Tap an item and then its matching pair.

Step 6 of 8

Predict Structural Compatibility

Predict assignment compatibility for typed product records, including the extra-property check for fresh object literals.

Match the required structure

Structural matching looks at available shape

TypeScript checks a record structurally: it asks whether the source makes every required Product property available with a compatible type. The source does not need to have been declared as Product.

An existing variable may carry extra properties and still be assigned to Product. The Product-annotated variable exposes the Product contract, while the original runtime record still retains its extra data.

One record can provide a larger shape

A larger catalog record with an extra featured property is assigned to a Product contract containing only its required and optional Product fields.

Compatible required fields allow the assignment; the extra property remains on the original record.

Type-checks: existing variable with extra data

typescript
const catalogEntry = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
  category: { id: 'home', label: 'Home' },
  description: 'Warm desk light',
  featured: true
};

const product: Product = catalogEntry;

Apply the fresh-literal check

Fresh object literals are checked for unexpected fields

A direct assignment of a fresh object literal gets one additional check. TypeScript flags properties that are not declared by the target contract. This excess-property check is useful because an unexpected field may be a typo.

The two sources below have the same fields, but one is an existing variable and the other is a fresh object literal written directly at the Product assignment.

Does not type-check: unexpected field in a fresh object literal

typescript
const product: Product = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
  category: { id: 'home', label: 'Home' },
  description: 'Warm desk light',
  featured: true
};

Warning

Correct the data or the contract deliberately

Here, featured is not declared by Product, so the direct literal does not type-check. Remove or correct an unintended field. Add a field to the shared contract only when it is genuinely part of every product the contract should describe.

Separate errors from allowed extras

Compare the source form and the property issue before deciding whether an assignment will type-check.

Compatibility outcomes for the current Product contract
SituationOutcomeReason
Existing variable has all Product fields plus `featured`Type-checksExtra source property is allowed
Fresh literal has all Product fields plus `featured`Does not type-checkUnexpected property is checked
Any source omits `category`Does not type-checkRequired nested record is absent
Any source uses a string for `price`Does not type-check`price` requires a number
A record omits `description`Type-checks`description` is optional

Check the runtime distinction

True or false: assigning an existing record with an extra property to Product deletes that extra property from the runtime record.

Apply structural compatibility

Classify and repair the assignments

Review these assignments.

const existingEntry = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
  category: { id: 'home', label: 'Home' },
  description: 'Warm desk light',
  featured: true
};
const existingProduct: Product = existingEntry;

const literalProduct: Product = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
  category: { id: 'home', label: 'Home' },
  description: 'Warm desk light',
  featured: true
};

const notebook: Product = {
  id: 'p-102',
  name: 'Notebook',
  price: '4.5',
  category: { id: 'office', label: 'Office' }
};

Classify the existingProduct and literalProduct assignments. Explain why their extra-property outcomes differ. Then state the change needed to make notebook type-check, without using an assertion.

Write at least 180 characters (0/180).

Step 7 of 8

Share Contracts Between Modules

Share reusable catalog contracts between modules while keeping type-only dependencies separate from runtime data imports.

1. Export shapes, not data

A module for catalog contracts

Put contracts in a contract module

Create src/catalog-types.ts for shapes that other modules can share. Export a named type alias with export type and a named interface with export interface.

These declarations describe data for the type checker. They do not create a product, a category, a constructor, or runtime validation.

Export the contracts

src/catalog-types.ts

typescript
export interface Category {
  id: string;
  label: string;
}

export type Product = {
  id: string;
  name: string;
  price: number;
  category: Category;
  description?: string;
};
A contract module shares Product and Category shapes with other modules through dotted type-only connections.

The contract module exports descriptions of records, not record data.

2. Create data in a separate module

Use Product for checking

Check data against an imported contract

In src/catalog-data.ts, use a type-only import when Product is needed only as an annotation. import type makes that intent explicit: TypeScript uses the contract while checking, then removes this dependency from the emitted JavaScript.

This module still creates runtime data itself: the product record and the exported products collection.

Create and export the catalog data

src/catalog-data.ts

typescript
import type { Product } from './catalog-types.ts';

const product: Product = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
  category: { id: 'home', label: 'Home' },
  description: 'Warm desk light'
};

export const products: Product[] = [
  product,
  {
    id: 'p-102',
    name: 'Notebook',
    price: 4.5,
    category: { id: 'office', label: 'Office' }
  }
];

Tip

What is checked

Product checks every collection element, including the nested category. The notebook may omit description because that property is optional; omission does not add a default value.

3. Keep type and value imports distinct

Consume the catalog at runtime

Import values as values

src/main.ts needs the actual products array in the browser, so it uses a normal import—not import type. A normal import creates a runtime module dependency because the imported value is used by JavaScript.

Run npx tsc --noEmit as the type check after creating these files.

Import the products value

src/main.ts

typescript
import { products } from './catalog-data.ts';

console.log(`Catalog products: ${products.length}`);

Two different dependencies

Catalog-data has a dotted type-only dependency on catalog-types, while main has a solid runtime dependency on catalog-data.

Type-only dependencies are erased; the products value remains available at runtime.

4. Practice: connect contracts and data

Complete the module connections

Complete the exports and imports. After completing the code, distinguish the erased type-only dependency from the import that supplies actual catalog data at runtime.

typescript
// src/catalog-types.ts
export  Product = {
  id: string;
  name: string;
  price: number;
  category: Category;
  description?: string;
};

export  Category {
  id: string;
  label: string;
}

// src/catalog-data.ts
import  { Product } from './catalog-types.ts';

const product: Product = {
  id: 'p-101',
  name: 'Desk Lamp',
  price: 29.99,
  category: { id: 'home', label: 'Home' },
  description: 'Warm desk light'
};

export const products: Product[] = [product];

// src/main.ts
 { products } from './catalog-data.ts';

Step 8 of 8

Apply and Review the Catalog Contracts

Complete and verify a reusable catalog model across contract, data, and entry modules.

Assemble the complete model

One catalog, three roles

A reusable catalog has three separate roles:

  • src/catalog-types.ts describes Category and Product for the checker. It creates no product records.
  • src/catalog-data.ts creates the runtime products array and uses a type-only import to check it.
  • src/main.ts imports products as runtime data.

Create or replace these files in your own src folder with the complete example below. The product records contain required nested categories; the lamp includes an optional description, while the notebook omits it.

Complete catalog modules

typescript
// src/catalog-types.ts
export interface Category {
  id: string;
  label: string;
}

export type Product = {
  id: string;
  name: string;
  price: number;
  category: Category;
  description?: string;
};

// src/catalog-data.ts
import type { Product } from './catalog-types.ts';

export const products: Product[] = [
  {
    id: 'p-101',
    name: 'Desk Lamp',
    price: 29.99,
    category: { id: 'home', label: 'Home' },
    description: 'Warm desk light'
  },
  {
    id: 'p-102',
    name: 'Notebook',
    price: 4.5,
    category: { id: 'office', label: 'Office' }
  }
];

// src/main.ts
import { products } from './catalog-data.ts';

console.log(products);

Contract dependency versus runtime data

A diagram separates an erased contract dependency from a runtime products dependency across three TypeScript modules.

The contract guides checking; the data module creates the records; the entry module receives runtime data.

Review before the type check

Catalog contract review

Review each record and module before relying on a successful type check. A type-only import is about the role of a dependency, while the other checks concern the data that is actually created.

Review checklist

Use this checklist when you repair catalog data.

Each check protects a different part of the catalog model.
AreaCheck
Required fieldsEvery product has id, name, price, and category.
Nested recordEach category has a string id and string label.
Optional fieldDescription may be omitted; when present it is a string.
CollectionThe products value is annotated as a Product[] collection.
Import kindProduct is brought into the data module with a type-only import.

Create and correct two diagnostics

To observe useful diagnostics locally, keep src/catalog-types.ts and src/main.ts from the previous screen, then temporarily replace only src/catalog-data.ts with this version. Run npx tsc --noEmit from your project root.

This file Does not type-check: the lamp lacks its required category, and the notebook supplies a number where description requires a string. Repair those properties, then run the type check again.

Does not type-check: src/catalog-data.ts

typescript
import type { Product } from './catalog-types.ts';

export const products: Product[] = [
  {
    id: 'p-101',
    name: 'Desk Lamp',
    price: 29.99
  },
  {
    id: 'p-102',
    name: 'Notebook',
    price: 4.5,
    category: { id: 'office', label: 'Office' },
    description: 4.5
  }
];

Apply every contract together

Repair the module-based catalog

Complete the integrated repair. The three snippets represent the contract module, the data module, and the entry module. Fill each marker so the catalog has an optional description, a type-only dependency, and an explicitly typed collection.

Integrated contract and import repair

Complete the missing TypeScript syntax in these module snippets.

typescript
// src/catalog-types.ts
export interface Category {
  id: string;
  label: string;
}

export type Product = {
  id: string;
  name: string;
  price: number;
  category: Category;
  description: string;
};

// src/catalog-data.ts
import  { Product } from './catalog-types.ts';

export const products:  = [
  {
    id: 'p-101',
    name: 'Desk Lamp',
    price: 29.99,
    category: { id: 'home', label: 'Home' },
    description: 'Warm desk light'
  },
  {
    id: 'p-102',
    name: 'Notebook',
    price: 4.5,
    category: { id: 'office', label: 'Office' }
  }
];

// src/main.ts
import { products } from './catalog-data.ts';

console.log(products);

Verify and consolidate

Verify the repaired catalog locally

On your computer, run the broken data-module example through npx tsc --noEmit, correct it, and run the type check again. Report one diagnostic you observed by naming the property and incompatibility, then state the exact correction you made. Also confirm that the final catalog has both nested categories, omits the notebook description, and uses the intended import kinds.

Write at least 100 characters (0/100).

Summary

Final review

TypeScript contracts describe assumptions for the checker. Product records and the products array are runtime values created by your data module. Exporting or importing a type does not create a constructor, a record, or runtime validation.

  • Use a named Product contract to check each record in a Product[] collection.
  • Nested required records must satisfy their own contract; optional properties may be omitted.
  • Structural checks compare available properties and their value types.
  • Use import type for contracts and ordinary imports for runtime values.
  • Run npx tsc --noEmit to type-check your local project.

Tutorial complete

Congratulations! You completed: Define Reusable Object and Array Types

You can now describe reusable product records and collections, verify their compatibility, and share contracts across modules without creating runtime data.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate