숏컷 코드
interface User {
id: string;
nickname?: string;
readonly createdAt: Date;
}객체 속성
객체 프로퍼티에서 먼저 나눠 볼 문법은 아래입니다.
- optional:
nickname?: string - readonly:
readonly id: string - 둘 다 함께:
readonly nickname?: string - optional + readonly 분리된 입력/출력 타입
1) 값이 빠질 수 있으면 optional
부분 설정 객체, API 응답, 선택 입력처럼 어떤 프로퍼티가 아예 없을 수 있다면 ?를 붙입니다.
interface Config {
timeout?: number;
}이때 사용하는 쪽에서는 undefined 가능성까지 같이 다뤄야 합니다.
2) 생성 후 바뀌면 안 되면 readonly
식별자, 생성 시각, 외부에서 수정되면 안 되는 필드에는 readonly가 잘 맞습니다.
interface User {
readonly id: string;
name: string;
}TypeScript 수준 제약이므로 런타임 freeze와는 다르지만, 설계 의도를 훨씬 정직하게 보여 줍니다.
3) 입력과 출력 계약을 다르게 잡을 때 특히 유용하다
생성 요청은 optional 필드를 받고, 저장된 엔티티는 readonly 식별자를 가진다는 식으로 역할을 나누면 모델링이 더 깔끔해집니다.
interface CreateUserInput {
nickname?: string;
}
interface User {
readonly id: string;
nickname?: string;
}생성 요청, 수정 요청, 저장된 엔티티를 같은 객체 타입 하나로 밀어 넣기보다 역할에 맞게 나누면 optional과 readonly의 의미가 훨씬 또렷해집니다.
4) optional은 "빈 문자열"이 아니라 "없음"까지 포함한다
nickname?: string은 값이 빈 문자열일 수도 있다는 뜻이 아니라, 프로퍼티 자체가 없을 수도 있다는 뜻입니다.
그래서 읽을 때는 항상 "존재 여부"를 같이 봐야 합니다.
const user: User = { id: "u1" };
if ("nickname" in user) {
console.log(user.nickname);
}5) readonly는 참조 재할당을 막을 뿐, 깊은 불변성 전체를 보장하진 않는다
기본 감각으로는 충분하지만, 중첩 객체 전체의 깊은 불변성과는 별개라는 점은 기억해야 합니다. 우선은 "이 필드를 다시 덮어써도 되는가"를 판단하는 용도로 읽는 편이 좋습니다.
사용 기준
체크포인트
- 값이 빠질 수 있다: optional
? - 생성 후 바뀌면 안 된다:
readonly - 부분 설정 객체다: optional 활용
- 식별자/타임스탬프 보호다:
readonly - optional을 읽는다: 존재 여부 분기 필요
주의할 점
optional 프로퍼티는 값이 없을 수 있다는 뜻이므로, 바로 메서드를 호출하면 오류가 납니다.
interface User {
nickname?: string;
}
// ❌ nickname이 없을 수 있음
function upper(user: User) {
return user.nickname.toUpperCase();
}
// ✅ 존재 여부를 먼저 확인
function upper(user: User) {
return user.nickname ? user.nickname.toUpperCase() : "";
}
// ❌ readonly는 런타임 freeze라고 착각
const user: User = { id: "u1", nickname: "mina" };
// user.id = "u2";참고 링크
1 sources