이펙티브 타입스크립트 (댄 밴더캄 지음) 를 읽고 정리
📍요약
✅readonly를 사용하면 의도치 않은 변경으로 인한 에러를 방지하고, 런타임 이전에 확인할 수 있다
✅readonly와 const는 차이가 있다
✅readonly는 shallow하게 동작한다
📍readonly 타입으로 문제 해결하기 1
0
0 + 1
0 + 1 + 2
...
같은 삼각수를 출력하는 함수를 만들었는데, 코드가 의도한 대로 동작하지 않는다
// 배열의 합을 계산하는 헬퍼 함수
function arraySum(arr: number[]) {
let sum = 0, num;
while ((num = arr.pop()) !== undefined) { // 여기서 pop 메서드가 실행되어 원본 배열이 수정됨
sum += num;
}
return sum;
}
function printTriangles(n: number) {
const nums = [];
for (let i = 0; i < n; i++) {
nums.push(i);
console.log(arraySum(nums));
}
}
console.log(printTriangles(5)) // 0 1 2 3 4
매개변수에 readonly 타입을 사용하여 매개변수가 훼손되는 것을 사전에 방지할 수 있다
// 배열의 합을 계산하는 헬퍼 함수
function arraySum(arr: readonly number[]) {
let sum = 0, num;
while ((num = arr.pop()) !== undefined) { // 에러 : readonly 에는 pop 속성이 없음
sum += num;
}
return sum;
}
그리고 arraySum 함수를 원본 배열을 수정하지 않게 하여 타입 에러를 해결하고 정상적으로 삼각수를 출력하도록 수정한다
// 배열의 합을 계산하는 헬퍼 함수
function arraySum(arr: readonly number[]) {
return arr.reduce((acc, cur) => acc + cur, 0);
}
function printTriangles(n: number) {
const nums = [];
for (let i = 0; i < n; i++) {
nums.push(i);
console.log(arraySum(nums));
}
}
console.log(printTriangles(5)) // 0 1 3 6 10
📍readonly 타입
⭐배열에 readonly를 부여한다면...
1️⃣배열의 요소를 수정할 수 없다
const test: readonly number[] = [1, 2, 3, 4, 5];
test[0] = 15 // 에러 : 요소를 읽기만 가능
2️⃣length를 변경할 수도 없다
(length를 인위적으로 늘리면 -> 기존 요소 변화 X, 줄이면 -> 기존 요소 없어짐)
3️⃣원본 배열을 변경하는 pop, push 같은 메서드 사용 불가
⭐예를들면 readonly number[] 는 number[] 의 서브 타입이므로,
readonly 타입인 변수에 변경 가능한 배열을 할당할 수 있지만, 반대는 불가능하다
- 어떤 함수를 readonly로 만들면, 그 함수를 호출하는 다른 함수도 모두 readonly 처럼 동작하게 만들어야 한다
- 다른 라이브러리의 함수를 호출하는 경우에는 타입 선언을 바꿀 수 없으므로 타입 단언문을 사용한다
예) as number[]
⭐readonly는 shallow하게 동작한다
- 객체가 원소인 배열에서는 객체를 수정할 수 있다는 것을 의미한다
- 객체에 사용되는 Readonly 제네릭 예시
interface Outer {
inner: {
x: number;
}
}
const o: Readonly<Outer> = { inner: { x: 0 }};
o.inner = { x: 1 };
// ~~~~ Cannot assign to 'inner' because it is a read-only property
o.inner.x = 1; // 객체 수정 가능
📍readonly 타입으로 문제 해결하기 2
소설처럼 몇 개의 행으로 구성된 문자열이 있다. 이 문자열을 빈 행으로 구분하여 배열에 넣어주는 프로그램을 만드려고 한다
function parseTaggedText(lines: string[]): string[][] {
const paragraphs: string[][] = [];
const currPara: string[] = [];
const addParagraph = () => {
if (currPara.length) {
paragraphs.push(currPara);
currPara.length = 0; // 배열을 비움
}
};
for (const line of lines) {
if (!line) {
addParagraph();
} else {
currPara.push(line);
}
}
addParagraph();
return paragraphs;
}
// 백틱으로 문자열을 나타내면 줄바꿈 형태가 보존됨
const text =
`Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Quisque ex sapien, consequat eget vulputate ut, lobortis in augue.
Integer vel aliquet purus. Suspendisse hendrerit nisi ac hendrerit cursus.
Vestibulum sed metus dapibus, congue ante sed, luctus orci.
Pellentesque urna sapien, lobortis bibendum vehicula quis, hendrerit sed augue.
Nulla non massa non est luctus accumsan. Phasellus iaculis tortor sem,
sed tincidunt tortor pulvinar ac. Integer pharetra ante sit amet mi pellentesque consequat.
Nam nisl sapien, euismod nec metus at, tempor maximus justo.`;
console.log(parseTaggedText(text.split("\n"))); // [[], [], []]
그런데 예상과 다르게 빈 배열이 출력된다
이를 해결하기 위해 우선 currPara에 readonly 타입을 부여한다
- 그리고 타입에러가 3곳에서 발생하고, 2 곳의 타입에러는 원본 배열을 수정하지 않도록 하여 해결할 수 있다
그런데 paragraphs에 readonly[]인 currPara를 넣으려 할때 아직 에러가 남아 있다.
- 이를 해결하는 3가지 방법이 있다
1️⃣currPara의 복사본 만들기
2️⃣paragraphs 에도 readonly 타입 부여하기
3️⃣currPara의 readonly를 제거하기 위해 타입 단언문 as 사용하기
function parseTaggedText(lines: string[]): string[][] {
const paragraphs: string[][] = [];
// const paragraphs: (readonly string[])[] = []; // 괄호 없으면 통째로 readonly // 에러 수정 2
let currPara: readonly string[] = [];
const addParagraph = () => {
if (currPara.length) {
paragraphs.push([...currPara]); // 타입 에러 수정 : 원본 배열을 변경하지 않게
// paragraphs.push([...currPara]); // 에러 수정 1
// paragraphs.push(currPara as string[]); // 에러 수정 3
currPara = []; // 에러 수정
}
};
for (const line of lines) {
if (!line) {
addParagraph();
} else {
currPara = [...currPara, line]; // 타입 에러 수정 : 원본 배열을 변경하지 않게
}
}
addParagraph();
return paragraphs;
}
const text =
`Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Quisque ex sapien, consequat eget vulputate ut, lobortis in augue.
Integer vel aliquet purus. Suspendisse hendrerit nisi ac hendrerit cursus.
Vestibulum sed metus dapibus, congue ante sed, luctus orci.
Pellentesque urna sapien, lobortis bibendum vehicula quis, hendrerit sed augue.
Nulla non massa non est luctus accumsan. Phasellus iaculis tortor sem,
sed tincidunt tortor pulvinar ac. Integer pharetra ante sit amet mi pellentesque consequat.
Nam nisl sapien, euismod nec metus at, tempor maximus justo.`;
console.log(parseTaggedText(text.split("\n"))); // 빈 행마다 잘라서 배열에 원소로 사용