객체의 속성과 메서드에 적용되는 특징을 알자
TypeScript는 객체의 구조를 엄격히 관리할 수 있도록 다양한 문법과 기능을 제공한다. 이번 글에서는 객체의 속성과 메서드에 적용할 수 있는 주요 특징들을 중심으로 살펴보자. 이는 interface나 type alias 중 무엇을 쓰든 공통적으로 적용되는 개념이다.
Optional과 Readonly 수식어
객체의 속성에는 ?를 붙여 옵셔널(optional)로 만들거나, readonly를 붙여 읽기 전용(read-only) 으로 만들 수 있다.
interface Example {
hello: string;
world?: number; // 옵셔널
readonly wow: boolean; // 읽기 전용
readonly multiple?: symbol; // 읽기 전용 + 옵셔널
}
const example: Example = {
hello: 'hi',
wow: false,
};
example.world = 42; // 가능 (옵셔널이라 추가해도 됨)
example.wow = true; // ❌ Error: read-only 속성은 수정 불가
world는 있어도 되고 없어도 되는 속성이므로 undefined도 허용된다.
객체 리터럴 vs 변수 할당의 타입 검사
TypeScript는 객체 리터럴을 직접 대입할 때와 변수를 통해 간접적으로 대입할 때 타입 검사를 다르게 수행한다.
interface Example {
hello: string;
}
const example1: Example = {
hello: 'hi',
why: 'unexpected', // ❌ Error: 'why'는 정의되지 않은 속성
};
const obj = {
hello: 'hi',
why: 'ok',
};
const example2: Example = obj; // ✅ 통과됨
왜 이런 차이가 생길까?
객체 리터럴을 직접 대입하면 잉여 속성 검사(Excess Property Checking) 가 실행되어 선언되지 않은 속성은 에러로 처리된다. 반면 변수를 통해 대입하면 호환성 검사만 수행되어 잉여 속성이 허용된다. 실무에서는 객체 리터럴로 직접 넘길 때 타입 오류가 발생하면, 중간 변수로 분리해 확인할 수도 있다.
함수에서도 발생하는 잉여 속성 검사
interface Money {
amount: number;
unit: string;
}
function addMoney(a: Money, b: Money): Money {
return { amount: a.amount + b.amount, unit: 'won' };
}
addMoney({ amount: 1000, unit: 'won', error: 'oops' }, { amount: 2000, unit: 'won' });
// ❌ Error: 'error'는 정의되지 않은 속성
마찬가지로 객체 리터럴을 직접 함수 인자로 넘기면 에러가 발생하지만, 변수를 통해 넘기면 통과 된다.
구조 분해 할당 시 올바른 타입 표기
const { prop: { nested } }: { prop: { nested: string } } = {
prop: { nested: 'hi', a: 1, b: true },
};
console.log(nested); // 'hi'
구조 분해 할당에서 변수명과 타입명을 혼동하지 않도록 주의해야 하자.
인덱스 접근 타입 (Indexed Access Type)
type Animal = {
name: string;
age: number;
};
type NameType = Animal['name']; // string
속성의 타입을 재사용하거나 연동하고 싶을 때 유용하다. 객체처럼 'key' 또는 "key"로 접근 가능하지만, Animal.name처럼 점 표기법은 사용할 수 없다.
keyof 연산자
const obj = {
hello: 'world',
name: 'zero',
age: 28,
};
type Keys = keyof typeof obj; // 'hello' | 'name' | 'age'
type Values = typeof obj[Keys]; // string | number
- keyof는 객체의 키 타입 유니언을 생성
- typeof는 변수의 타입을 추론
- typeof obj[Keys]로 값 타입 유니언을 생성 가능
메서드 선언 방식 세 가지
interface Example {
a(): void;
b: () => void;
c: {
(): void;
};
}
매핑된 객체 타입 (Mapped Object Type)
인덱스 시그니처의 한계를 매핑된 타입으로 해결 할 수 있다. 리터럴 유니언 타입은 인덱스 시그니처에서 사용할 수 없다.
type HelloAndHi = {
[key: 'hello' | 'hi']: string; // ❌ Error
};
type HelloAndHi = {
[key in 'hello' | 'hi']: string;
};
// 결과: { hello: string; hi: string; }
기존 타입 복사하기
interface Original {
name: string;
age: number;
married: boolean;
}
type Copy = {
[key in keyof Original]: Original[key];
};
TS에서 기존 객체 타입을 복사할 수 있으며, 수식어도 추가하거나 제거할 수 있다.
// 수식어 추가
type ReadOnlyOptional = {
readonly [key in keyof Original]?: Original[key];
};
// 수식어 제거
type Cleaned = {
-readonly [key in keyof ReadOnlyOptional]-?: ReadOnlyOptional[key];
};
속성 이름 변경
type Copy = {
[key in keyof Original as Capitalize<key>]: Original[key];
};
// 결과: { Name: string; Age: number; Married: boolean }
as와 Capitalize, Uncapitalize, Uppercase, Lowercase 같은 유틸리티 타입을 조합하면 객체의 키 이름도 변경 가능하다.
개념 | 설명 |
? | 속성을 옵셔널로 만듦 |
readonly | 속성을 읽기 전용으로 만듦 |
keyof | 객체의 키 유니언 타입 추출 |
인덱스 접근 타입 | 속성의 값 타입을 추출 |
잉여 속성 검사 | 객체 리터럴에만 적용되는 추가 속성 검사 |
매핑된 타입 | 기존 타입 기반으로 새로운 타입 생성 |
수식어 제거/추가 | -readonly, +readonly |
속성 이름 변경 | as Capitalize<key> 등 활용 |