개념/배경

readonly는 TypeScript 전용 타입 수정자이며 JavaScript 런타임에는 존재하지 않음 컴파일 타임에만 효력이 있으며, 읽기 전용으로 선언된 배열·객체 속성을 수정하려는 코드에 대해 타입 오류를 발생시킴

핵심 개념

배열 또는 객체 속성의 변경 금지 의도를 타입 수준에서 명시 예를 들어 onChainResult: readonly bigint[]는 블록체인에서 읽어온 결과 배열이 이후 코드 흐름에서 변형되지 않음을 보장하려는 의도 표현 읽기 전용 제약은 타입 체크 시점에만 적용되며, 트랜스파일된 JavaScript에는 제약이 남지 않음

동작과 제한

읽기 전용 배열은 변경 메서드가 제거된 ReadonlyArray 형태로 다뤄짐 객체의 readonly 속성은 재할당 금지, 단 객체 참조 자체는 다른 값으로 교체 가능할 수 있으므로 설계 시 구분 필요 타입 수준 readonly는 얕은 불변성에 해당하는 경우가 많음. 중첩된 구조까지 불변으로 보장하려면 계층별로 readonly를 적용하거나 별도 불변 모델을 설계해야 함

사용 예시

// 읽기 전용 배열
const numbers: readonly number[] = [1, 2, 3]
numbers.push(4) // Error: Property 'push' does not exist on type 'readonly number[]'
numbers[0] = 0  // Error: Index signature in type 'readonly number[]' only permits reading

// 읽기 전용 객체 속성
interface User {
  readonly id: number
  name: string
}

const user: User = { id: 1, name: "John" }
user.name = "Jane" // OK
user.id = 2        // Error: Cannot assign to 'id' because it is a read-only property

위 패턴은 데이터 무결성 요구가 있는 도메인에서 의도를 드러내고 실수성 변이를 사전에 차단하는 데 유용함

JavaScript Object.freeze와의 차이

Object.freeze는 런타임 동작이며 객체를 동결하지만 기본적으로 얕게만 적용됨. 중첩 객체는 별도 처리 필요 TypeScript readonly는 컴파일 타임 제약으로, 런타임 오버헤드를 추가하지 않음 두 방법은 상호 보완적이며, 타입 안전성과 런타임 보호가 동시에 필요하면 병행 고려

const arr = Object.freeze([1, 2, 3])
arr.push(4) // Error: Cannot add property 3, object is not extensible

환경이나 모드에 따라 예외 발생 또는 무시 동작이 다를 수 있으므로 런타임 보호에 의존하는 경우 주의

주의와 베스트프랙티스

외부 시스템에서 유입되는 데이터, 캐시 스냅샷, 계산 결과 등 변형되지 않아야 하는 값은 readonly로 모델링 함수 인자에 readonly 배열을 사용해 호출자와 구현 간 변이 계약을 명확히 표현 const는 변수 재할당 금지, readonly는 구조 내부 변경 금지라는 서로 다른 개념임을 구분 필요 시 중첩 구조에 대한 불변성은 계층적으로 명시하거나 유틸리티 타입과 패턴을 조합해 강화

마무리

readonly는 변이 금지 의도를 타입으로 명확히 하고, 실수성 수정을 컴파일 단계에서 차단해 코드 품질과 안정성을 높이는 도구임

참고자료