| |

TypeScript 9 ๐Ÿ”ท Object Types and Optional Properties

Object types are how TypeScript describes structured data โ€” records, entities, configuration, API responses, component props. Almost every non-trivial type in a real codebase is an object type, and almost every object type has at least one field that might be missing. That’s where optional properties come in: the ? modifier, the interaction with undefined, the difference between “missing” and “present but undefined,” and how all of that flows through assignment, reading, and narrowing. Getting object types right โ€” especially optional fields โ€” is the difference between types that catch bugs and types that quietly allow them.

Key point: An object type is a shape โ€” a set of property names and their types. It’s structural, not nominal: any value with the right shape matches. Optional properties (name?: string) mean the property may be absent, and TypeScript tracks that fact through every read and write. The distinction between name?: string and name: string | undefined matters more than it looks.


Object type syntax

An object type lists properties and their types.

type User = {
  id: number;
  name: string;
  email: string;
};

Same with an interface:

interface User {
  id: number;
  name: string;
  email: string;
}

Both describe the same shape. Use interface for object shapes by convention (see Chapter 7), type when you need a union or a more complex alias.

Property separators: Commas, semicolons, or newlines all work. Semicolons are the TypeScript convention.

type A = { id: number; name: string; };   // semicolons
type B = { id: number, name: string };    // commas
type C = {                                // newlines
  id: number;
  name: string;
};

Types of properties can be anything โ€” primitives, other object types, unions, functions, arrays.

interface Product {
  id: string;
  name: string;
  price: number;
  tags: string[];
  metadata: { created: Date; updated: Date };
  getDisplayName: () => string;
}

Nesting: Object types nest freely. Each level is its own shape.

interface Order {
  id: string;
  customer: {
    id: string;
    name: string;
    address: {
      street: string;
      city: string;
      zip: string;
    };
  };
  items: Array<{
    sku: string;
    quantity: number;
  }>;
}

That’s a five-level shape. TypeScript checks every level, every property, every call site.

Why structural typing matters here: A User isn’t a class instance. It’s any value with id: number, name: string, and email: string. That means object literals, JSON-parsed data (with validation), and instances of other classes all qualify if their shapes match. TypeScript follows JavaScript’s model โ€” shapes, not names.


Optional properties โ€” ?

A property marked with ? may be absent from the object.

interface User {
  id: number;
  name: string;
  nickname?: string;              // may be missing
}

const alice: User = { id: 1, name: 'Alice' };                  // โœ…
const bob: User = { id: 2, name: 'Bob', nickname: 'Bobby' };   // โœ…

Both are valid User values. nickname is optional.

The type of an optional property when read:

alice.nickname;                   // string | undefined

Reading nickname gives string | undefined โ€” because it may be absent, and if absent it’s undefined. TypeScript adds undefined to the property’s type automatically.

You must handle the undefined case:

alice.nickname.length;            // โŒ possibly undefined
alice.nickname?.length;           // โœ…
alice.nickname ?? 'Anonymous';    // โœ…

Writing an optional property:

alice.nickname = 'Al';            // โœ…
alice.nickname = undefined;       // โœ… โ€” explicit
delete alice.nickname;            // โœ… โ€” removes the property

All three are allowed. undefined and absence are both valid for an optional property.

? only applies to properties, not to the enclosing object: nickname?: string doesn’t make alice itself optional. It just makes the property omittable.

Why ? adds undefined: An optional property can be missing. When you read a missing property in JavaScript, you get undefined. TypeScript models that reality โ€” reading user.nickname gives string | undefined, forcing you to handle the missing case. This is the single most useful thing optional properties do: they make absence explicit in the type.


Optional vs | undefined

name?: string and name: string | undefined look similar but mean different things.

interface A {
  name?: string;                  // optional โ€” may be absent
}

interface B {
  name: string | undefined;       // required โ€” may be undefined
}

Assignment:

const a1: A = {};                                    // โœ…
const a2: A = { name: 'Alice' };                     // โœ…
const a3: A = { name: undefined };                   // โœ…

const b1: B = {};                                    // โŒ missing name
const b2: B = { name: 'Alice' };                     // โœ…
const b3: B = { name: undefined };                   // โœ…

For A, all three are valid โ€” name may be omitted, present, or explicitly undefined.

For B, name is required. It must be present, but its value may be undefined.

The difference in one sentence: ? means “may be missing”; | undefined means “must be present, may be undefined.”

Which to use:

GoalSyntax
Property may be omitted entirelyname?: T
Property always present, may be undefinedname: T | undefined
Property may be omitted OR explicit nullname?: T | null
Property always present, may be nullname: T | null

When the difference matters: Consider a PATCH API payload.

interface UserPatch {
  name?: string;
  email?: string;
}

If the client doesn’t send name, the server treats it as “don’t change the name.” That’s different from sending name: undefined, which might mean “set the name to undefined.” The type system can express the difference โ€” pick the form that matches the semantics.

exactOptionalPropertyTypes โ€” the strict option:

Under this compiler flag, name?: string forbids name: undefined explicitly.

// With exactOptionalPropertyTypes: true
const a: A = { name: undefined };   // โŒ
const a: A = {};                    // โœ…

The strictest reading: an optional property is either present with a T value or absent โ€” never present with undefined. This catches bugs where code accidentally sets an optional field to undefined instead of omitting it. It’s off by default; enable it if your team wants that precision.

Why the distinction is real: In JavaScript, { name: undefined } and {} differ in 'name' in obj. Some libraries and APIs care. TypeScript’s default treats them the same for optional properties, but exactOptionalPropertyTypes makes the type system track the difference. Use it when it matters.


Readonly properties

A property marked readonly can’t be reassigned after creation.

interface User {
  readonly id: number;
  name: string;
}

const alice: User = { id: 1, name: 'Alice' };
alice.name = 'Alicia';            // โœ…
alice.id = 2;                     // โŒ readonly

readonly is compile-time only โ€” the property is still mutable at runtime. It documents intent and catches accidental writes.

Readonly is shallow:

interface Config {
  readonly server: {
    host: string;
    port: number;
  };
}

const cfg: Config = { server: { host: 'localhost', port: 8080 } };
cfg.server = { host: 'x', port: 1 };  // โŒ readonly
cfg.server.host = 'x';                // โœ… inner object isn't readonly

Only the top-level property is protected. For deep immutability, you need recursive readonly types or as const.

Readonly arrays and tuples:

interface Data {
  readonly items: readonly string[];
}

const d: Data = { items: ['a', 'b'] };
d.items.push('c');                // โŒ
d.items = ['x'];                  // โŒ

readonly T[] prevents mutation of the array contents; readonly before the property name prevents reassignment of the property itself.

Readonly in classes:

class User {
  readonly id: number;

  constructor(id: number) {
    this.id = id;                 // โœ… assignment in constructor allowed
  }

  changeId() {
    // this.id = 2;               // โŒ after construction
  }
}

readonly on class properties allows assignment in the constructor only.

Why readonly is useful: It documents which fields should never change and prevents accidental writes. For domain objects like IDs and creation timestamps, this catches real bugs. It’s also enforced through parameter types: a function accepting readonly User[] can read but not mutate the elements.


Object type modifiers summary

ModifierSyntaxEffect
Optionalname?: TMay be absent
Readonlyreadonly name: TCan’t be reassigned
Index signature[key: string]: TDynamic keys
Methodfn(): TCall signature as property
Function propertyfn: (x: T) => UFunction-typed property

Methods vs function properties:

interface A {
  greet(): string;                // method
}

interface B {
  greet: () => string;            // function property
}

They’re structurally compatible but subtly different. Method syntax has this typing and supports declaration merging better; function property syntax is more precise about mutability. For most cases they’re interchangeable.

Index signatures:

interface Dict {
  [key: string]: number;
}

Allows any string key with a number value. Useful for maps and JSON-like data. See Chapter 5 for indexed access.

Why these modifiers matter: They turn a bare object type into a precise contract. readonly id: number says “never change this.” nickname?: string says “may be missing.” [key: string]: number says “any key, number values.” Each modifier is a small constraint that adds up to types that catch real mistakes.


Object literals โ€” excess property checks

TypeScript checks object literals for excess properties โ€” extra keys that aren’t in the target type.

interface User {
  id: number;
  name: string;
}

const a: User = { id: 1, name: 'Alice', age: 30 };   // โŒ excess property 'age'

The object literal has age, which isn’t on User. TypeScript rejects it.

But it only applies to object literals โ€” direct assignments. Through a variable, excess properties are allowed:

const obj = { id: 1, name: 'Alice', age: 30 };
const b: User = obj;              // โœ… โ€” obj has at least User's shape

This is the structural typing rule: as long as a value has the required properties, extras are fine. Excess property checks are a special case for object literals to catch typos.

Where excess checks help:

interface Config {
  apiUrl: string;
  timeout: number;
}

const bad: Config = {
  apiUrl: 'https://x',
  timeout: 5000,
  tmieout: 10000              // โŒ typo โ€” caught by excess check
};

Without excess checks, tmieout would be silently allowed as an extra property, and the typo would never be caught.

Working around excess checks:

// Option 1: variable
const cfg = { apiUrl: 'x', timeout: 5, extra: true };
const good: Config = cfg;         // โœ…

// Option 2: spread
const good2: Config = { apiUrl: 'x', timeout: 5, ...({ extra: true }) };  // โœ…

// Option 3: add an index signature
interface Config {
  apiUrl: string;
  timeout: number;
  [key: string]: unknown;         // allow extras
}

Use these deliberately โ€” excess checks catch typos, and working around them should be a conscious choice.

Why excess checks exist but only for literals: Object literals are usually written inline for a specific target type. If you typed tmieout and meant timeout, the typo is right there. Variables may carry extra properties for other reasons, so the check is relaxed. The rule catches the common case without being overly strict.


Reading and narrowing optional properties

Optional properties require narrowing before use.

interface User {
  id: number;
  nickname?: string;
}

function greet(u: User): string {
  if (u.nickname) {
    return `Hello, ${u.nickname}`;    // u.nickname is string here
  }
  return `Hello, user #${u.id}`;
}

Truthiness narrowing removes undefined and also the empty string. If empty-string nicknames are valid, use !== undefined instead:

if (u.nickname !== undefined) {
  return `Hello, ${u.nickname}`;      // string, but '' is allowed
}

Checking for property presence:

if ('nickname' in u) {
  // u.nickname is string | undefined
  // the property exists, but might be undefined
}

in narrows to “the property exists” but not “the value isn’t undefined.” Under default TypeScript settings, 'nickname' in u gives u.nickname: string in the if branch โ€” because for optional properties, “exists” and “not undefined” are usually the same. Under exactOptionalPropertyTypes, in doesn’t remove undefined.

?. and ?? are the standard tools:

const display = u.nickname ?? u.name;
const length = u.nickname?.length;

Optional chaining short-circuits on undefined and null. Nullish coalescing provides a fallback only when the value is undefined or null โ€” not for '' or 0.

Destructuring with defaults:

const { nickname = 'Anonymous' } = u;
// nickname: string

The default removes undefined from the destructured variable’s type.

Why narrowing is essential for optional: Without narrowing, every optional field access would either fail to compile or require ?. everywhere. Narrowing โ€” via if, ?., or defaults โ€” lets you tell TypeScript “I know this is present” and then use it directly. That’s the ergonomic payoff of tracking nullability.


Object types in practice

Function parameters:

function sendEmail(message: {
  to: string;
  subject: string;
  body: string;
}): void {
  // ...
}

Inline object types in parameters work but become unwieldy. Extract them:

interface Email {
  to: string;
  subject: string;
  body: string;
}

function sendEmail(message: Email): void { }

Return values:

function makeUser(name: string): { id: string; name: string } {
  return { id: crypto.randomUUID(), name };
}

Extracting is cleaner:

interface User {
  id: string;
  name: string;
}

function makeUser(name: string): User { }

Partial objects: Use Partial<T> (from Chapter 2’s utility types, covered later in depth) to make all properties optional.

interface User {
  id: number;
  name: string;
  email: string;
}

type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string }

Required objects: Use Required<T> to make all properties required.

type Config = Required<{
  apiUrl?: string;
  timeout?: number;
}>;
// { apiUrl: string; timeout: number }

Pick and Omit: Select subsets of properties.

type UserSummary = Pick<User, 'id' | 'name'>;
type UserWithoutId = Omit<User, 'id'>;

These utility types are covered in depth in later chapters.

Why extracting object types matters: Inline object types in parameters become unreadable after two properties. Named types are shorter to use and easier to update. When you change Email, every function that accepts it is checked automatically. That’s the whole point of named types โ€” one place to change, everywhere checked.


A full example

A config system with optional, readonly, and index-signature properties.

interface ServerConfig {
  readonly host: string;
  readonly port: number;
  readonly ssl?: boolean;              // optional
  readonly timeout?: number;           // optional
}

interface AppConfig {
  readonly name: string;
  readonly version: string;
  readonly server: ServerConfig;
  readonly features: readonly string[];
  readonly metadata?: Record<string, string>;  // optional map
}

function createServer(config: ServerConfig): string {
  const { host, port, ssl = false, timeout = 30_000 } = config;
  const protocol = ssl ? 'https' : 'http';
  return `${protocol}://${host}:${port} (timeout: ${timeout}ms)`;
}

function hasFeature(config: AppConfig, feature: string): boolean {
  return config.features.includes(feature);
}

const config: AppConfig = {
  name: 'my-app',
  version: '1.0.0',
  server: {
    host: 'localhost',
    port: 8080
  },
  features: ['auth', 'logging']
};

console.log(createServer(config.server));
console.log(hasFeature(config, 'auth'));
console.log(hasFeature(config, 'metrics'));

// Readonly enforcement
// config.name = 'other';            // โŒ
// config.server.host = 'x';         // โŒ
// config.features.push('y');        // โŒ

Every optional field (ssl, timeout, metadata) is safely omitted. Every readonly field is protected. Destructuring with defaults handles the optional values cleanly.

Why this shape: It models a real configuration file โ€” required and optional keys, nested objects, read-only settings, and a list of feature flags. The type system prevents accidental mutation and requires you to handle optional fields correctly. That’s how object types pay off in production.


Complete Example Session

# ============================================
# PART 1: BASIC OBJECT TYPES
# ============================================

cat > objects.ts << 'EOF'
interface User {
  id: number;
  name: string;
  email: string;
}

const alice: User = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com'
};

console.log(alice);
EOF

npx tsc --noEmit objects.ts
# (no errors)

# ============================================
# PART 2: OPTIONAL PROPERTIES
# ============================================

cat > optional.ts << 'EOF'
interface User {
  id: number;
  name: string;
  nickname?: string;
}

const a: User = { id: 1, name: 'Alice' };
const b: User = { id: 2, name: 'Bob', nickname: 'Bobby' };

function display(u: User): string {
  return u.nickname ?? u.name;
}

console.log(display(a));
console.log(display(b));
EOF

npx tsc --noEmit optional.ts
# (no errors)

# ============================================
# PART 3: OPTIONAL VS | undefined
# ============================================

cat > distinction.ts << 'EOF'
interface A {
  name?: string;                  // may be absent
}

interface B {
  name: string | undefined;       // required, may be undefined
}

const a1: A = {};                 // โœ…
const a2: A = { name: undefined }; // โœ…

const b1: B = { name: undefined }; // โœ…
// const b2: B = {};              // โŒ missing name

console.log(a1, a2, b1);
EOF

npx tsc --noEmit distinction.ts
# (no errors)

# ============================================
# PART 4: READONLY
# ============================================

cat > readonly.ts << 'EOF'
interface User {
  readonly id: number;
  name: string;
}

const u: User = { id: 1, name: 'Alice' };
u.name = 'Alicia';                // โœ…
// u.id = 2;                      // โŒ readonly

interface Config {
  readonly features: readonly string[];
}

const c: Config = { features: ['a', 'b'] };
// c.features.push('x');          // โŒ
// c.features = ['y'];            // โŒ

console.log(u, c);
EOF

npx tsc --noEmit readonly.ts
# (no errors)

# ============================================
# PART 5: EXCESS PROPERTY CHECKS
# ============================================

cat > excess.ts << 'EOF'
interface User {
  id: number;
  name: string;
}

// โŒ excess property
// const a: User = { id: 1, name: 'Alice', age: 30 };

// โœ… via variable
const obj = { id: 1, name: 'Alice', age: 30 };
const b: User = obj;

console.log(b);
EOF

npx tsc --noEmit excess.ts
# (no errors)

# ============================================
# PART 6: FULL EXAMPLE
# ============================================

cat > config.ts << 'EOF'
interface ServerConfig {
  readonly host: string;
  readonly port: number;
  readonly ssl?: boolean;
  readonly timeout?: number;
}

interface AppConfig {
  readonly name: string;
  readonly version: string;
  readonly server: ServerConfig;
  readonly features: readonly string[];
}

function createServer(config: ServerConfig): string {
  const { host, port, ssl = false, timeout = 30_000 } = config;
  return `${ssl ? 'https' : 'http'}://${host}:${port} (${timeout}ms)`;
}

const cfg: AppConfig = {
  name: 'my-app',
  version: '1.0.0',
  server: { host: 'localhost', port: 8080 },
  features: ['auth']
};

console.log(createServer(cfg.server));
console.log(cfg.features.includes('auth'));
EOF

npx tsc --noEmit config.ts
# (no errors)

# ============================================
# PART 7: COMPILE AND RUN
# ============================================

npx tsc objects.ts optional.ts distinction.ts readonly.ts excess.ts config.ts
node objects.js
# [ { id: 1, name: 'Alice', email: 'alice@example.com' } ]

node optional.js
# [ Alice ]
# [ Bobby ]

node readonly.js
# [ { id: 1, name: 'Alicia' } { features: [ 'a', 'b' ] } ]

node config.js
# [ http://localhost:8080 (30000ms) ]
# [ true ]

Quick Reference

Object Type Syntax

FormMeaning
{ a: T; b: U }Object with a and b
interface X { a: T }Named shape
type X = { a: T }Alias
{ a?: T }Optional property
{ readonly a: T }Readonly property
{ [k: string]: T }Index signature
{ fn(): T }Method
{ fn: () => T }Function property

Property Modifiers

ModifierSyntaxEffect
Optionala?: TMay be absent
Readonlyreadonly a: TCan’t be assigned
Bothreadonly a?: TOptional and readonly

Optional vs | undefined

FormRequiredAccepts undefinedCan omit
a?: TโŒโœ…โœ…
a: T | undefinedโœ…โœ…โŒ
a: Tโœ…โŒโŒ
a?: T | nullโŒโœ… + nullโœ…
a: T | nullโœ…โœ… + nullโŒ

Reading Optional Properties

CodeType
u.nicknamestring | undefined
u.nickname?.lengthnumber | undefined
u.nickname ?? 'x'string
if (u.nickname) { u.nickname }string (narrowed)
if (u.nickname !== undefined) { }string (narrowed)

Readonly Effects

LocationBehavior
Interface propertyCan’t reassign
Class propertyAssignable only in constructor
readonly T[]Array contents immutable
Parameter readonly T[]Caller’s array can’t be mutated
Deep readonlyNot automatic โ€” use recursive types

Excess Property Checks

ContextCheck applies
Object literal assigned to typed variableโœ…
Via intermediate variableโŒ
Via spreadโŒ
Via as castโŒ
Function argument (literal)โœ…

Utility Combinations

TypeResult
Partial<User>All properties optional
Required<User>All properties required
Readonly<User>All properties readonly
Pick<User, 'id'>Only those properties
Omit<User, 'id'>All except those

exactOptionalPropertyTypes

Setting{ a?: T } allows { a: undefined }
false (default)โœ…
trueโŒ โ€” must be absent

Best Practices

โœ… Do This:

// Use interfaces for object shapes
interface User { id: number; name: string; }             // โœ…

// Mark optional fields with `?`
interface User { nickname?: string; }                    // โœ…

// Use `readonly` for immutable fields
interface User { readonly id: number; }                  // โœ…

// Handle optional reads with `?.` or `??`
const n = user.nickname ?? 'Anonymous';                  // โœ…

// Use `readonly T[]` for immutable arrays
interface Data { items: readonly string[]; }             // โœ…

// Extract inline object types
interface Email { to: string; subject: string; }         // โœ…

// Use utility types for transformations
type Update = Partial<User>;                             // โœ…

// Destructure with defaults for optional fields
const { nickname = 'Anon' } = user;                      // โœ…

โŒ Don’t Do This:

// Don't use | undefined when ? is cleaner
interface U { name: string | undefined; }                // โš ๏ธ  required

// Don't access optional fields without narrowing
user.nickname.length;                                    // โŒ possibly undefined

// Don't expect readonly to be deep
interface C { readonly inner: { x: number }; }
c.inner.x = 5;                                            // โœ… โ€” inner isn't readonly

// Don't work around excess checks lightly
const u: User = { id: 1, name: 'A', extra: 2 } as any;   // โš ๏ธ  hides typos

// Don't write huge inline object types
function f(data: { a: string; b: number; c: boolean; d: Date; }): void { }  // โš ๏ธ  extract

// Don't mix `?` and `| undefined` inconsistently
interface A { x?: number; y: number | undefined; }       // โš ๏ธ  pick one convention

// Don't ignore the empty-string case with truthiness
if (user.nickname) { }  // '' skipped                     // โš ๏ธ  use !== undefined

Common Pitfalls

PitfallProblemSolution
? vs | undefined confusionWrong assignment rulesKnow which you need
Accessing optional without narrowingCompile errorUse ?. or if
Readonly is shallowInner objects mutableRecursive readonly or as const
Excess check missedTypos sneak throughAssign object literals directly
Working around excess checksReal typos hiddenOnly use as deliberately
Truthiness skips ''Empty string treated as missingUse !== undefined
? doesn’t make the object optionalDifferent concept?: on the property, not the object
readonly on arraysStill pushesUse readonly T[]
Deleting readonly propCompile errorDon’t
Object literal excess checkUnexpected errorAssign via variable or spread

Real-World Examples

1. Basic user

interface User {
  id: number;
  name: string;
  email: string;
}

2. Optional field

interface User {
  id: number;
  name: string;
  nickname?: string;
}

3. Readonly ID

interface User {
  readonly id: number;
  name: string;
}

4. Optional and readonly

interface Config {
  readonly apiUrl: string;
  readonly timeout?: number;
}

5. Nested object

interface Order {
  id: string;
  customer: { id: string; name: string };
}

6. Readonly array

interface Data {
  readonly items: readonly string[];
}

7. Optional array

interface Data {
  items?: string[];
}

8. Index signature

interface Dict {
  [key: string]: number;
}

9. Partial

type UserUpdate = Partial<User>;

10. Pick

type UserSummary = Pick<User, 'id' | 'name'>;

11. Omit

type UserWithoutId = Omit<User, 'id'>;

12. Destructure with defaults

const { nickname = 'Anon' } = user;

13. Nullish coalescing

const display = user.nickname ?? user.name;

14. Optional chaining

const city = user.address?.city;

15. Function parameter

function send(msg: { to: string; body: string }): void { }

16. Return object

function makeUser(name: string): User {
  return { id: '1', name, email: 'a@b.c' };
}

17. Excess check

const u: User = { id: 1, name: 'Alice', typo: true };  // โŒ

18. Excess check via variable

const obj = { id: 1, name: 'Alice', extra: true };
const u: User = obj;                                    // โœ…

19. Method property

interface Calc {
  sum(a: number, b: number): number;
}

20. Function property

interface Calc {
  sum: (a: number, b: number) => number;
}

Visual: Optional Property Behavior

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  interface User {                            โ”‚
โ”‚    id: number;                               โ”‚
โ”‚    nickname?: string;                        โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Valid values:                               โ”‚
โ”‚  { id: 1 }                  โœ…               โ”‚
โ”‚  { id: 1, nickname: 'Al' }  โœ…               โ”‚
โ”‚  { id: 1, nickname: undefined } โœ… (default) โ”‚
โ”‚                                              โ”‚
โ”‚  Read type of nickname:                      โ”‚
โ”‚  string | undefined                          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: ? vs | undefined

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  a?: T                                       โ”‚
โ”‚                                              โ”‚
โ”‚  {}                       โœ…                 โ”‚
โ”‚  { a: value }             โœ…                 โ”‚
โ”‚  { a: undefined }         โœ… (default)       โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ May be absent                             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  a: T | undefined                            โ”‚
โ”‚                                              โ”‚
โ”‚  {}                       โŒ missing         โ”‚
โ”‚  { a: value }             โœ…                 โ”‚
โ”‚  { a: undefined }         โœ…                 โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ Must be present, may be undefined         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Readonly Shallow vs Deep

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Shallow โ€” only the top level                โ”‚
โ”‚                                              โ”‚
โ”‚  interface C {                               โ”‚
โ”‚    readonly server: { host: string };        โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  c.server = { host: 'x' };  โŒ               โ”‚
โ”‚  c.server.host = 'x';       โœ…               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Deep โ€” with recursive Readonly              โ”‚
โ”‚                                              โ”‚
โ”‚  type DeepReadonly<T> = {                    โ”‚
โ”‚    readonly [K in keyof T]: DeepReadonly<T[K]>;โ”‚
โ”‚  };                                          โ”‚
โ”‚                                              โ”‚
โ”‚  All levels readonly โ€” see mapped types.     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Excess Property Checks

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Direct literal assignment                   โ”‚
โ”‚                                              โ”‚
โ”‚  const u: User = {                           โ”‚
โ”‚    id: 1,                                    โ”‚
โ”‚    name: 'Alice',                            โ”‚
โ”‚    age: 30         โ† extra                   โ”‚
โ”‚  };                                          โ”‚
โ”‚                                              โ”‚
โ”‚  โŒ Excess property 'age'                     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Via intermediate variable                   โ”‚
โ”‚                                              โ”‚
โ”‚  const obj = { id: 1, name: 'Alice', age: 30 };โ”‚
โ”‚  const u: User = obj;                        โ”‚
โ”‚                                              โ”‚
โ”‚  โœ… Allowed โ€” obj has at least User's shape   โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Object Type Composition

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  interface User {                            โ”‚
โ”‚    readonly id: number;                      โ”‚
โ”‚    name: string;                             โ”‚
โ”‚    email?: string;                           โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  id      โ†’ required, readonly                โ”‚
โ”‚  name    โ†’ required, mutable                 โ”‚
โ”‚  email   โ†’ optional, mutable                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Mapped: Partial<User>                       โ”‚
โ”‚                                              โ”‚
โ”‚  {                                           โ”‚
โ”‚    id?: number;                              โ”‚
โ”‚    name?: string;                            โ”‚
โ”‚    email?: string;                           โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  readonly removed, all optional              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Mapped: Readonly<Partial<User>>             โ”‚
โ”‚                                              โ”‚
โ”‚  {                                           โ”‚
โ”‚    readonly id?: number;                     โ”‚
โ”‚    readonly name?: string;                   โ”‚
โ”‚    readonly email?: string;                  โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Narrowing Optional

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  u.nickname                                  โ”‚
โ”‚  type: string | undefined                    โ”‚
โ”‚                                              โ”‚
โ”‚  u.nickname?.length                          โ”‚
โ”‚  type: number | undefined                    โ”‚
โ”‚                                              โ”‚
โ”‚  u.nickname ?? 'Anonymous'                   โ”‚
โ”‚  type: string                                โ”‚
โ”‚                                              โ”‚
โ”‚  if (u.nickname) {                           โ”‚
โ”‚    // u.nickname is string                   โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  if (u.nickname !== undefined) {             โ”‚
โ”‚    // string, including ''                   โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Method vs Function Property

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Method syntax                               โ”‚
โ”‚                                              โ”‚
โ”‚  interface A {                               โ”‚
โ”‚    greet(): string;                          โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  โ€ข this typed via interface                  โ”‚
โ”‚  โ€ข supports overloads                        โ”‚
โ”‚  โ€ข bivariant parameter check (looser)        โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Function property syntax                    โ”‚
โ”‚                                              โ”‚
โ”‚  interface B {                               โ”‚
โ”‚    greet: () => string;                      โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  โ€ข no special this                            โ”‚
โ”‚  โ€ข stricter contravariance                   โ”‚
โ”‚  โ€ข more precise for objects                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptMeaning
Object typeShape of properties and their types
?Property may be absent
readonlyProperty can’t be reassigned
| undefinedProperty required, may be undefined
Index signatureAny key with a given value type
Method syntaxfn(): T
Function propertyfn: () => T
Excess property checkReject extra keys in object literals
Utility typesPartial, Required, Readonly, Pick, Omit

Key takeaways:

  • Object types describe shapes โ€” TypeScript checks structure, not names
  • Object types nest freely โ€” every level is checked
  • ? makes a property optional โ€” may be absent or undefined
  • | undefined makes the property required but its value may be undefined
  • These two differ in what they allow at the call site โ€” know which you need
  • readonly prevents reassignment โ€” and is shallow, not deep
  • readonly T[] prevents array mutation โ€” use for immutability
  • Excess property checks reject extra keys in object literals, but not through variables
  • Narrowing is required to use optional properties โ€” ?., ??, if, or destructuring with defaults
  • exactOptionalPropertyTypes makes optional properties forbid explicit undefined
  • Utility types (Partial, Required, Readonly, Pick, Omit) transform object shapes โ€” covered in depth later
  • Extract inline object types into named interfaces โ€” better errors, easier updates

Remember: Object types are the workhorse of TypeScript. Almost every type you write describes an object shape โ€” a User, an Order, a config, a payload. Optional properties add flexibility but require narrowing. Readonly properties add safety but are shallow. The ? and | undefined distinction matters for what the API allows. Get these right, and the bulk of your type system does what it’s supposed to: catches mistakes before they reach production, and documents your domain in code that the compiler enforces.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!