const, as const, readonly 알아보기

이희제·2024년 8월 13일
post-thumbnail

현재 업무를 진행하면서 as const를 활용해서 타입 추론 범위를 좁히면서 상수 성격의 데이터 값을 관리하고 있다.

이와 더불어 const, readonly까지 비교해보면서 알아보면 좋을 것 같다.

1. const

제일 먼저 const는 let과 대부분 세트로 언급이 될 것이다. const, let 모두 블록 스코프를 지니고 TDZ에 속한다.(참고)

let과 달리 const를 사용하면 값의 재할당이 불가능하다.

예를 들어 아래와 같이 코드를 작성해보자.

const testValue = "testValue"

testValue = "testValue2" // 불가능

아래와 같이 testValue는 constant한 값이기 때문에 재할당이 불가능하다고 뜰 것이다.

그렇다면 객체를 const로 선언하고 객체의 속성을 바꾸게 되면 어떻게 될까?

const testObj = {
    a: "a",
    b: "b"
}

testObj.a = "c" // ok

정상적으로 변경되는 것을 확인할 수 있다. testObj 자체의 메모리 주소값은 변경이 일어나지 않았기 때문에 가능한 것이다.

다음과 같이 testObj 를 아예 다른 값으로 할당을 하여 메모리 주소값을 변경하게 되면 오류가 발생한다.

const testObj = {
    a: "a",
    b: "b"
}

testObj = {c: "d"} // ❌ impossible 


2. readonly

readonly는 값, 속성에 사용되는 것이 아닌 type, interface, class에 사용된다. 즉 타입 정의에 적용할 수 있다.

다음과 같이 interface를 사용해서 타입은 선언하고 적용해보자.

interface Person {
  readonly name: string;
  age: number;
}

const person: Person = { name: "Alice", age: 30 };
person.name = "Bob"; // 오류 발생: 'name'은 읽기 전용입니다.
person.age = 31;     // 정상: 'age'는 수정 가능

readonly로 설정되어 있는 name의 값은 변경할 수 없다.

배열에서도 마찬가지이다.

const numbers: readonly number[] = [1, 2, 3];
numbers[0] = 10;    // 오류 발생: 배열의 요소는 읽기 전용입니다.
numbers.push(4);    // 오류 발생: 읽기 전용 배열에 요소를 추가할 수 없습니다.

-------------------------------------------------------------------
  
let strings: readonly string[] = ["a", "b", "c"]

strings.push("d") // 오류 발생: 읽기 전용 배열에 요소를 추가할 수 없습니다.

strings = strings.concat(["d"]) // let으로 선언되었기 때문에 재할당 가능, concat은 새로운 배열을 반환한다

Readonly라는 유틸리티 타입도 존재한다.

interface Todo {
  title: string;
}
 
const todo: Readonly<Todo> = {
  title: "Delete inactive users",
};
 
todo.title = "Hello"; // 오류 발생

어떻게 구현되어 있는 지 보면 다음과 같다.

type Readonly<T> = { readonly [P in keyof T]: T[P]; }

맵드 타입을 통해서 각 속성을 readonly로 만들어주는 것을 확인할 수 있을 것이다.

다만, 주의할 점은 Readonly 유틸리티 타입은 얕게 동작한다는 것이다.

interface Info {
  title: string;
  person :{
    name: string
  }
}
 
const info: Readonly<Info> = {
  title: "Delete inactive users",
  person:{
    name: "name"
  }
};

info.person = { name: "newName" } // Cannot assign to 'person' because it is a read-only property
info.person.name = "newName" // 문제 없음

타입 별칭을 통해 어떻게 타입이 구성되었는 지 확인해보자.

person만 readonly가 적용되어 있고 person.name은 readonly가 적용되어 있지 않은 것을 볼 수 있다.


3. as const

as const는 상수 어설션(const assertions)이라고 부른다. const와 달리 타입스크립트의 기능이다. 사용하면 타입 추론의 범위를 좁혀 더 안전하게 값을 사용할 수 있다.

아래와 같은 객체가 있다고 가정해보자.

const LOCALE = {
    en: "EN_US",
    ko: "KO_KR",
    ja: "JP_JA"
}

타입스크립트는 다음과 같이 객체의 타입을 추론한다.

en, ko, ja 모두 string 타입으로 추론된다. 하지만 내가 의도한 바는 각 언어 키 값은 단순 string 타입이 아닌 더 좁은 언어 코드 형태의 리터럴 타입으로 하는 것이다.

따라서 여기서 as const를 사용해 리터럴 값으로 타입을 좁힐 수 있다.

const LOCALE = {
    en: "EN_US",
    ko: "KO_KR",
    ja: "JP_JA"
} as const

타입이 좁혀짐과 동시가 객체의 모든 속성이 readonly가 되었다. 따라서 객체의 각 속성도 변경할 수 없다.

const LOCALE = {
    en: "EN_US",
    ko: "KO_KR",
    ja: "JP_JA"
} as const

LOCALE.en = "US" // ❌ Cannot assign to 'en' because it is a read-only property.

추가로 위 LOCALE의 속성 값들을 가지는 유니온 타입도 쉽게 생성할 수 있다.

type LanguageCode = typeof LOCALE[keyof typeof LOCALE]; // 'EN_US' | 'KO_KR' | 'JP_JA'

더 나아가 as const와 Object.freeze()의 차이점을 알아보자. as const는 위에서 봤으니 Object.freeze()를 중심으로 보자.

  • as const: 컴파일 타임에 타입 정보를 추론하고 수정한다. 런타임에는 영향을 미치지 않는다.
  • Object.freeze: 런타임에 객체를 동결하여 속성의 변경과 삭제를 막는다.
const car = Object.freeze({
  brand: 'Hyundai',
  model: 'sonata',
})

car.brand = 'Honda' // ❌ 런타임 에러: Cannot assign to read only property 'brand' of object '#<Object>'

Object.freeze()로 감싸진 객체의 타입은 다음과 같이 속성에 readonly가 붙여진 상태로 추론된다.

앞서 봤던 Readonly 유틸리티 타입과 동일하게 Object.freeze()도 얕게 동작한다.

따라서, 런타임에서 특정 데이터 변경을 방지하려면 Object.freeze()를 사용하면 된다.


➕ 추가

진짜 마지막으로 Object.freeze()와 Object.seal()의 차이점이다.

1. Object.freeze

  • Object.freeze는 객체를 동결(freeze)하여 속성의 추가, 삭제, 변경을 막는다.
  • 동결된 객체의 속성은 읽기만 가능하며, 쓰기나 삭제는 불가능.
"use strict";
const user = {
  username: 'johndoe',
  email: 'johndoe@example.com',
  profile: {
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
  },
};

Object.freeze(user);

user.username = 'janedoe'; // ❌ 런타임 에러: Cannot assign to read only property 'username' of object '#<Object>'

2. Object.seal

  • Object.seal은 객체를 밀봉(seal)하여 새로운 속성의 추가와 기존 속성의 삭제를 막는다.
  • 기존 속성의 값은 변경할 수 있다.
"use strict";
const person = {
  name: 'John',
  age: 30,
};

Object.seal(person);

person.name = 'Jane'; // ✅ 기존 속성 값 변경 가능
person.job = 'Developer'; // ❌ 런타임 에러: Cannot add property job, object is not extensible
delete person.age; // ❌ 런타임 에러: Cannot delete property 'age' of #<Object>
profile
그냥 하자

0개의 댓글