
현재 업무를 진행하면서 as const를 활용해서 타입 추론 범위를 좁히면서 상수 성격의 데이터 값을 관리하고 있다.
이와 더불어 const, readonly까지 비교해보면서 알아보면 좋을 것 같다.
제일 먼저 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

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가 적용되어 있지 않은 것을 볼 수 있다.
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()의 차이점이다.
"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>'
"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>