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
Userisn’t a class instance. It’s any value withid: number,name: string, andemail: 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
?addsundefined: An optional property can be missing. When you read a missing property in JavaScript, you getundefined. TypeScript models that reality โ readinguser.nicknamegivesstring | 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:
| Goal | Syntax |
|---|---|
| Property may be omitted entirely | name?: T |
Property always present, may be undefined | name: T | undefined |
Property may be omitted OR explicit null | name?: T | null |
Property always present, may be null | name: 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, butexactOptionalPropertyTypesmakes 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
readonlyis 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 acceptingreadonly User[]can read but not mutate the elements.
Object type modifiers summary
| Modifier | Syntax | Effect |
|---|---|---|
| Optional | name?: T | May be absent |
| Readonly | readonly name: T | Can’t be reassigned |
| Index signature | [key: string]: T | Dynamic keys |
| Method | fn(): T | Call signature as property |
| Function property | fn: (x: T) => U | Function-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: numbersays “never change this.”nickname?: stringsays “may be missing.”[key: string]: numbersays “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
tmieoutand meanttimeout, 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 โ viaif,?., 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
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
| Form | Meaning |
|---|---|
{ 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
| Modifier | Syntax | Effect |
|---|---|---|
| Optional | a?: T | May be absent |
| Readonly | readonly a: T | Can’t be assigned |
| Both | readonly a?: T | Optional and readonly |
Optional vs | undefined
| Form | Required | Accepts undefined | Can omit |
|---|---|---|---|
a?: T | โ | โ | โ |
a: T | undefined | โ | โ | โ |
a: T | โ | โ | โ |
a?: T | null | โ | โ + null | โ |
a: T | null | โ | โ + null | โ |
Reading Optional Properties
| Code | Type |
|---|---|
u.nickname | string | undefined |
u.nickname?.length | number | undefined |
u.nickname ?? 'x' | string |
if (u.nickname) { u.nickname } | string (narrowed) |
if (u.nickname !== undefined) { } | string (narrowed) |
Readonly Effects
| Location | Behavior |
|---|---|
| Interface property | Can’t reassign |
| Class property | Assignable only in constructor |
readonly T[] | Array contents immutable |
Parameter readonly T[] | Caller’s array can’t be mutated |
| Deep readonly | Not automatic โ use recursive types |
Excess Property Checks
| Context | Check applies |
|---|---|
| Object literal assigned to typed variable | โ |
| Via intermediate variable | โ |
| Via spread | โ |
Via as cast | โ |
| Function argument (literal) | โ |
Utility Combinations
| Type | Result |
|---|---|
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
| Pitfall | Problem | Solution |
|---|---|---|
? vs | undefined confusion | Wrong assignment rules | Know which you need |
| Accessing optional without narrowing | Compile error | Use ?. or if |
| Readonly is shallow | Inner objects mutable | Recursive readonly or as const |
| Excess check missed | Typos sneak through | Assign object literals directly |
| Working around excess checks | Real typos hidden | Only use as deliberately |
Truthiness skips '' | Empty string treated as missing | Use !== undefined |
? doesn’t make the object optional | Different concept | ?: on the property, not the object |
readonly on arrays | Still pushes | Use readonly T[] |
| Deleting readonly prop | Compile error | Don’t |
| Object literal excess check | Unexpected error | Assign 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
| Concept | Meaning |
|---|---|
| Object type | Shape of properties and their types |
? | Property may be absent |
readonly | Property can’t be reassigned |
| undefined | Property required, may be undefined |
| Index signature | Any key with a given value type |
| Method syntax | fn(): T |
| Function property | fn: () => T |
| Excess property check | Reject extra keys in object literals |
| Utility types | Partial, 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 orundefined| undefinedmakes the property required but its value may beundefined- These two differ in what they allow at the call site โ know which you need
readonlyprevents reassignment โ and is shallow, not deepreadonly 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 exactOptionalPropertyTypesmakes optional properties forbid explicitundefined- 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!