TypeScript의 타입 시스템은 인터페이스 속성을 readonly (읽기 전용)으로 지정할 수 있게 해줍니다. 이를 통해 함수형 방식이 가능해집니다 (예상되지 않은 상태 변경은 나쁨):
function foo(config: {
readonly bar: number,
readonly bas: number
}) {
// ..
}
let config = { bar: 123, bas: 123 };
foo(config);
// `config`가 변경되지 않는다고 확신할 수 있음 🌹
당연히 readonly는 interface와 type 정의에서 모두 사용할 수 있습니다, 예를 들면:
type Foo = {
readonly bar: number;
readonly bas: number;
}
// 초기화는 오케이
let foo: Foo = { bar: 123, bas: 456 };
// 변경은 안됨
foo.bar = 456; // 오류: 상수나 읽기 전용 속성은 대입(assignment) 표현식의 좌항이 될 수 없음
클래스의 속성도 readonly로 선언할 수 있습니다. 이런 속성은 선언 시점에 초기화하거나 아래 보인 것처럼 생성자(constructor)에서 초기화할 수 있습니다:
class Foo {
readonly bar = 1; // OK
readonly baz: string;
constructor() {
this.baz = "hello"; // OK
}
}
Readonly
타입 매핑을 이용하여 T 타입을 받고 그 타입의 모든 속성을 readonly로 지정하는 Readonly라는 타입이 있습니다. 여기에 실제로 이걸 사용하는 데모가 나와 있습니다:
type Foo = {
bar: number;
bas: number;
}
type FooReadonly = Readonly<Foo>;
let foo: Foo = {bar: 123, bas: 456};
let fooReadonly: FooReadonly = {bar: 123, bas: 456};
foo.bar = 456; // 오케이
fooReadonly.bar = 456; // 오류: bar는 readonly임
다양한 사용 사례
ReactJS
불변성(immutability)을 사랑하는 라이브러리로 ReactJS가 있는데, Props와 State를 불변하는 것으로 표시할 수 있습니다, 예를 들면:
interface Props {
readonly foo: number;
}
interface State {
readonly bar: number;
}
export class Something extends React.Component<Props,State> {
someMethod() {
// 아래와 같은 일이 발생할 수 없음을 확신할 수 있음
this.props.foo = 123; // 오류: (props는 불변임)
this.state.baz = 456; // 오류: (this.setState를 사용해야 함)
}
}
하지만 React의 타입 정의가 이들을 이미 readonly로 표기하고 있기 때문에 직접 이렇게 할 필요는 없습니다 (내부적으로 인자로 전달된 제네릭 타입을 앞서 설명한 Readonly 타입으로 감쌈).
export class Something extends React.Component<{ foo: number }, { baz: number }> {
// 아래와 같은 일이 발생할 수 없음을 확신할 수 있음
someMethod() {
this.props.foo = 123; // 오류: (props는 불변임)
this.state.baz = 456; // 오류: (this.setState를 사용해야 함)
}
}
이건 JavaScript 네이티브 배열을 불변 스타일로 사용하고자 할 때 아주 편합니다. 사실 TypeScript는 이런 일을 할 수 있도록 ReadonlyArray<T> 인터페이스를 이미 제공하고 있습니다:
let foo: ReadonlyArray<number> = [1, 2, 3];
console.log(foo[0]); // 오케이
foo.push(4); // 오류: `push`는 배열을 상태를 바꾸므로 ReadonlyArray에 존재하지 않음
foo = foo.concat([4]); // 오케이: 복사본 생성
자동 추론
경우에 따라 컴파일러가 자동으로 특정 항목이 읽기 전용임을 추론해낼 수 있습니다. 즉, 클래스에 속성이 있지만 게터(getter)만 있고 세터(setter)가 없다면 컴파일러는 그 속성을 readonly인 것으로 간주합니다. 예를 들면:
class Person {
firstName: string = "John";
lastName: string = "Doe";
get fullName() {
return this.firstName + this.lastName;
}
}
const person = new Person();
console.log(person.fullName); // John Doe
person.fullName = "Dear Reader"; // 오류! fullName은 readonly
const와의 차이점
const
변수 참조를 위한 것
변수에 다른 값을 할당/대입할 수 없음.
readonly
속성을 위한 것
속성을 앨리어싱을 통해 변경될 수 있음
1을 설명하는 예제:
const foo = 123; // 변수 참조
var bar: {
readonly bar: number; // 속성의 경우
}
2를 설명하는 예제:
let foo: {
readonly bar: number;
} = {
bar: 123
};
function iMutateFoo(foo: { bar: number }) {
foo.bar = 456;
}
iMutateFoo(foo); // foo 인자가 foo 파라미터에 의해 앨리어싱됨
console.log(foo.bar); // 456!
기본적으로 readonly는 내가 속성을 변경하지 못함을 보장하지만, 객체를 다른 사람에게 넘길 경우에는 이것이 보장되지 않고 그 다른 사람은 객체의 속성을 변경할 수 있습니다 (타입 호환성 문제 때문에 허용됨). 물론 iMutateFoo를 foo.bar를 변경하지 않는 함수로 선언했다면 아래에 보이는 것처럼 컴파일러가 올바르게 잘못을 지적할 수 있습니다:
interface Foo {
readonly bar: number;
}
let foo: Foo = {
bar: 123
};
function iTakeFoo(foo: Foo) {
foo.bar = 456; // 오류! bar는 readonly
}
iTakeFoo(foo); // // foo 인자가 foo 파라미터에 의해 앨리어싱됨