배열을 인자로 받아 연산하는 함수는 인자가 참조하는 배열의 원본을 바꿀 수 있기 때문에 사이드 이펙트를 원치 않는 함수에서는 readonly 접근 제어자를 사용할 수 있다.
const a: number[] = [1, 2, 3];
const b: readonly number[] = a; // 정상
const c: number[] = b; // 오류
const arr = [ 1, 2, 3, 4 ];
const sum = (arr: readonly number []) => { // readonly 매개변수
return arr.reduce((prev, cur) => prev + cur, 0);
}
sum(arr);
만약 함수가 매개변수를 변경하지 않고도 제어가 가능하다면 readonly로 선언하면 된다. 그런데 어떤 함수를 readonly로 만들면, 그 함수를 호출하는 다른 함수도 모두 readonly로 만들어야 한다.
const와 readonly는 모두 값을 변경할 수 없는 변수를 만드는 데 사용되는 ts의 키워드이다.
상수의 타입:
const 키워드로 선언된 변수는 값이 할당될 때 타입을 추론하므로 상수의 타입을 명시적으로 선언해야 한다. 반면, readonly 키워드로 선언된 속성은 변수 타입이 이미 지정되어 있기 때문에 타입 선언이 필요하지 않다.
불변성의 범위:
const는 변수 자체가 변경할 수 없다. 그러나 변수에 할당된 객체는 변경할 수 있다. 반면, readonly는 변수 자체와 변수에 할당된 객체 모두 변경할 수 없다.
객체 리터럴 타입:
const는 객체 리터럴 타입에 사용될 수 있다. readonly는 객체 리터럴 타입에 사용될 수 없지만, 속성이 읽기 전용일 경우 사용할 수 있다.
const obj = { name: "John" };
obj.name = "Mike"; // 유효한 할당
console.log(obj); // { name: "Mike" }
class Person {
readonly name: string;
constructor(name: string) {
this.name = name;
}
}
const person = new Person("John");
person.name = "Mike"; // 유효하지 않은 할당
console.log(person.name); // "John"
=> readonly 는 객체를 변경할 수 없다는 뜻이지, 변수에 재할당이 불가능하다는 의미가 아니다.
readonly가 얕게 동작한다는 것은, 객체나 배열의 내부 요소까지 불변성을 보장하지 않는다는 것을 의미한다.
interface Person {
name: string;
age: number;
}
const person: Readonly<Person> = {
name: "John",
age: 30,
};
person.name = "Mike"; // 에러: Cannot assign to 'name' because it is a read-only property.
const people: ReadonlyArray<Person> = [
{ name: "John", age: 30 },
{ name: "Jane", age: 25 },
];
people[0].name = "Mike"; // 에러: Cannot assign to 'name' because it is a read-only property.
위 코드에서, person 변수와 people 배열은 Readonly 타입으로 선언되어 있다. 따라서 이들은 변경될 수 없다.
그러나 person 변수의 경우, name과 age 속성은 모두 변경될 수 없는 반면 people 배열의 경우, 배열 자체는 변경할 수 없지만, 배열의 요소들은 변경될 수 있다. 이는 readonly가 얕게 동작한다는 것을 보여주는 예시이다.
따라서, ts에서 객체나 배열의 내부 요소의 불변성을 보장하려면, Deep Readonly 타입을 사용하거나, 직접 객체나 배열 내부의 요소들을 모두 readonly로 선언해야 한다.
매핑된 타입을 사용해서 관련된 값과 타입을 동기화하고, 인터페이스에 새로운 속성을 추가할 때 선택을 강제하도록 매핑된 타입을 고려해야 한다.
(💥매핑(Mapping)은 TypeScript에서 타입을 변환하는 과정 중 하나로, 하나의 타입에서 다른 타입으로 변환하는 것을 말한다. 매핑된 타입(Mapped Type)은 기존 타입에서 새로운 타입을 생성하는 것이며, { [P in K]: T }와 같은 형태로 사용된다.)
ts에서 매핑된 타입은 다른 타입을 기반으로 새로운 타입을 만드는 기능이다. 이를 활용하여 값을 동기화하는 것은 매우 유용한 기능 중 하나이다.
값을 동기화한다는 것은 여러 변수나 객체들의 값을 일치시키는 것을 의미하는데 이를 통해 값이 변경될 때마다 모든 변수나 객체를 일일이 수정하는 번거로움을 피할 수 있다.
예를 들어
interface Person {
name: string;
age: number;
}
let person: Person = {
name: "John",
age: 30,
};
let personCopy = {
name: person.name,
age: person.age,
};
위 코드에서 person 객체와 personCopy 객체는 동일한 값을 가지고 있다. 그러나 person 객체의 값을 변경하면, personCopy 객체의 값을 일일이 변경해주어야 한다.
이를 매핑된 타입을 사용하여 해결할 수 있는데 다음과 같이 SyncedPerson 타입을 정의한다.
type SyncedPerson<T> = {
[K in keyof T]: T[K];
};
따라서, SyncedPerson 타입은 T 타입과 동일한 타입을 가지지만, 모든 속성들이 읽기 전용이며, 값을 변경할 수 없다.
( [K in keyof T]는 T 타입의 모든 속성들에 대해서 매핑된 타입을 생성하겠다는 의미
T[K]는 T 타입의 K 속성의 값을 의미)
이제, 위의 코드를 수정하면
interface Person {
name: string;
age: number;
}
type SyncedPerson<T> = {
readonly [K in keyof T]: T[K];
};
let person: Person = {
name: "John",
age: 30,
};
let personCopy: SyncedPerson<Person> = person;
코드에서 personCopy 변수는 person 변수와 동일한 값을 가지지만, 모든 속성들이 읽기 전용으로 선언되어 있다. 따라서 person 객체의 값을 변경하면, personCopy 객체의 값도 자동으로 변경된다.
이를 통해 값의 동기화를 쉽게 구현할 수 있으며, 코드의 중복성과 오류를 줄일 수 있다.