본문으로 건너뛰기

배열과 튜플

타입스크립트 목차기본 타입과 함수레슨 8 / 51

목차

배열 타입은 원소의 타입 뒤에 []를 붙여 number[]처럼 적습니다. 검사기는 배열 리터럴의 원소에서 이 타입을 추론하고, push로 넣는 값과 꺼낸 값을 그 원소 타입으로 봅니다. 자리마다 타입이 정해진 배열은 튜플(tuple)이라고 합니다.

자바스크립트 배열에는 무엇이든 섞여 들어가서, 수 배열에 글자 하나가 끼면 합계가 글자 잇기로 바뀌어도 오류가 나지 않습니다. 검사기는 그런 값을 넣는 줄을 막습니다. 다만 꺼낸 자리에 값이 정말 있는지는 보지 않습니다. 이 레슨의 마지막 절에서 봅니다.

7단계 중 1단계. 1번 줄: 원소가 모두 수라서 검사기는 prices를 number[]로 봅니다.

1배열 타입은 원소 타입 뒤에 []

JS 다시 보기: push는 배열 끝에 값을 더하고, Node는 배열을 [ 4000, 4500 ]처럼 찍습니다(JS '배열과 이름 둘').

[4000, 4500]처럼 대괄호로 그 자리에 쓴 배열을 배열 리터럴(array literal)이라고 합니다. const prices = [4000, 4500];⁠에서 검사기는 원소가 모두 수인 것을 보고 prices의 선언 타입을 number[]로 정합니다(검사 규칙 3). number[]은 'number 원소만 드는 배열'이라고 읽습니다. 글자 배열 ['커피', '라테']는 string[]입니다. 원소가 있는 배열은 이렇게 추론에 맡기고, 타입을 적는 것은 빈 배열일 때입니다(예제).

push도 인자를 넘기는 부르기입니다. 검사기는 넘기는 값이 원소 타입 모음에 드는지 봅니다(검사 규칙 2).

const prices = [4000, 4500];
prices.push('3500');
console.log(prices);
검사 결과 오류 1개
main.ts(2,13): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
실행 결과
[ 4000, 4500, '3500' ]

이 TS2345는 'string 타입 인자는 number 타입 매개변수에 넣을 수 없다', 곧 number 원소 자리에 글자 '3500'을 넣으려 했다는 뜻입니다. prices의 push가 받는 매개변수는 원소 타입 number입니다. Node는 검사하지 않으니 그대로 돌려, 수 배열 끝에 글자가 섞입니다. 자리를 골라 바꾸는 prices[0] = '무료';도 같은 까닭으로 TS2322가 납니다.

2튜플은 자리마다 타입이 정해진 배열

글자와 수를 섞어 쓴 ['라테', 4500]은 튜플이 아닙니다. 검사기는 원소 타입을 합쳐 (string | number)[]로 추론합니다. string | number는 'string이거나 number'라는 뜻이고(4단원 유니언), 괄호 뒤 []는 그런 원소가 몇 개든 드는 배열이라는 뜻입니다.

자리마다 타입을 정하려면 선언 타입을 [string, number]로 적습니다. 첫 자리는 string, 둘째 자리는 number이고, 길이도 둘로 정해집니다.

const row: [string, number] =
  ['라테', 4500];
console.log(row[0], row[1]);
console.log(row[2]);
검사 결과 오류 1개
main.ts(4,17): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.
실행 결과
라테 4500
undefined

1~2번 줄은 폰 화면에 맞추려고 = 뒤에서 나눈 한 문장입니다. TS2493은 '길이가 2인 튜플 [string, number]에는 2번 자리 원소가 없다'는 뜻입니다. 튜플은 길이를 알아서, 끝 너머를 꺼내는 4번 줄을 검사기가 막습니다. Node로 돌리면 4번 줄은 오류 없이 undefined를 찍습니다.

3꺼낸 값은 실행 때 없을 수 있다

JS 다시 보기: 배열에 없는 자리를 꺼내면 오류 없이 undefined가 나옵니다(JS '배열과 이름 둘').

튜플과 달리 string[]은 길이를 정하지 않은 타입입니다. 검사기는 몇 번째 자리를 꺼내든 지금 타입을 원소 타입 string으로 봅니다. 아래 3번 줄은 검사를 지나갈까요? Node로 돌리면 무엇이 찍힐까요? 답을 정한 뒤 펼쳐 보세요.

const orders = ['커피', '라테'];
const third = orders[2];
console.log(third.length);
검사 결과와 실행 결과 보기
검사 결과 오류 없음
실행 결과
main.ts:3console.log(third.length);                  ^ TypeError: Cannot read properties of undefined (reading 'length')    at main.ts:3:19

예상과 다르면 그곳이 배울 곳입니다. 검사는 오류 없이 지나갑니다. third의 지금 타입이 string이라 length가 있다고 보기 때문입니다. Node로 돌리면 orders에 2번 자리가 없어 third는 undefined이고, 3번 줄에서 멈춥니다. 'undefined의 속성을 읽을 수 없다(length를 읽다가)'는 뜻입니다.

검사를 지났다고 꺼낸 값이 늘 있는 것은 아닙니다. 이 과목 tsconfig는 꺼낸 값에 undefined를 더해 보는 옵션(noUncheckedIndexedAccess — 10단원)을 켜지 않았습니다. 자리가 없을 수 있다는 것을 검사기에게도 보이려면 orders.at(2)처럼 at()으로 꺼냅니다. 그러면 third의 지금 타입이 string | undefined가 되어 3번 줄이 TS18048(undefined일 수 있다)로 막히고, 거르는 법은 4단원의 'null·undefined 거르기'에서 배웁니다.

예제

오늘 가격 가운데 4000원보다 싼 것만 새 배열에 모읍니다. 자바스크립트의 거르기 틀에 타입을 붙인 꼴로, 빈 배열에는 원소 타입을 적습니다.

JS 다시 보기: for...of는 배열에서 값을 하나씩 꺼내 이름을 붙이고 덩어리를 실행합니다(JS 'for...of로 배열 돌기').

const prices = [4500, 3500];
const cheap: number[] = [];
for (const p of prices) {
  if (p < 4000) {
    cheap.push(p);
  }
}
console.log(cheap);
검사 결과 오류 없음
실행 결과
[ 3500 ]

3번 줄의 p는 prices의 원소 타입 number로 정해집니다. 5번 줄은 number 값을 number[]에 넣으니 검사를 지납니다. 2번 줄에 타입을 적지 않으면 어떻게 되는지는 자주 틀리는 곳 셋째에서 봅니다.

자주 틀리는 곳

틀린 생각: 검사를 지난 배열 꺼내기는 실행할 때도 값이 있다.

검사기가 꺼낸 값에 string 같은 타입을 붙여 주니, 그 자리에 값도 있다고 여기게 됩니다. 검사기는 배열의 길이를 세지 않습니다.

const orders = ['커피', '라테'];
const n = orders.length;
for (let i = 0; i <= n; i++) {
  console.log(orders[i].length);
}
검사 결과 오류 없음
실행 결과
22main.ts:4  console.log(orders[i].length);                        ^ TypeError: Cannot read properties of undefined (reading 'length')    at main.ts:4:25

반복 조건을 i < n이 아니라 i <= n으로 써서, i가 2일 때도 덩어리를 돕니다. 검사는 지나가지만, 실행은 셋째 바퀴에서 없는 2번 자리를 꺼내 멈춥니다.

맞는 규칙: string[]은 길이가 몇이든 string 원소만 드는 배열의 모음이라, 검사기는 몇 번째 자리를 꺼내도 string으로 봅니다. 자리가 정말 있는지는 실행할 때 정해지므로, 꺼내기 전에 길이로 따집니다(검사 규칙 2).

틀린 생각: 글자와 수를 섞어 쓴 배열 리터럴은 튜플이 된다.

['라테', 4500]은 겉보기에 '이름 · 가격' 짝이라 자리마다 타입이 정해진 것처럼 보입니다. 그러나 검사기는 원소 타입을 합친 배열로 봅니다.

const a = ['라테', 4500];
const b: [string, number] = a;
검사 결과 오류 1개
main.ts(2,7): error TS2322: Type '(string | number)[]' is not assignable to type '[string, number]'.  Target requires 2 element(s) but source may have fewer.

a의 타입 (string | number)[]은 원소가 몇 개든 되는 배열이라, 원소가 꼭 둘이어야 하는 튜플 자리에 넣을 수 없습니다. 아래 들여 쓴 줄이 까닭입니다 — '받는 쪽은 원소 2개가 있어야 하는데, 넣는 쪽은 그보다 적을 수 있다.'

맞는 규칙: 검사기는 배열 리터럴의 원소 타입을 합쳐 (string | number)[]처럼 추론합니다. 자리마다 타입이 정해진 짝이 필요하면 선언 타입을 [string, number]로 적습니다(검사 규칙 3).

틀린 생각: 빈 배열 []도 쓰다 보면 검사기가 원소 타입을 알맞게 정해 준다.

원소가 있는 배열은 타입을 적지 않아도 되니, 빈 배열도 그럴 것 같습니다. 그러나 빈 배열에는 원소 타입을 정할 첫 값이 없습니다.

const prices = [4500, 3500];
const cheap = [];
for (const p of prices) {
  if (p < 4000) {
    cheap.push(p + '원');
  }
}
console.log(cheap);
검사 결과 오류 없음
실행 결과
[ '3500원' ]

2번 줄 cheap에 마우스를 올리면 any[]입니다. any는 어떤 타입이든 된다는 표시이고, any인 곳은 검사기가 검사하지 않습니다(6단원 'any는 검사를 끈다'). 그래서 5번 줄에서 수 대신 글자를 넣어도 검사를 지납니다. 넣은 뒤인 8번 줄에서는 넣은 값을 따라 string[]으로 보이지만, 어느 쪽이든 넣는 값을 막지 않습니다. 2번 줄을 예제처럼 const cheap: number[] = [];⁠로 적으면 5번 줄이 TS2345로 막힙니다.

맞는 규칙: 빈 배열은 첫 값이 없어 원소 타입을 추론할 수 없습니다. 빈 배열에는 number[]처럼 원소 타입을 적습니다(검사 규칙 3).

확인 문제

1. 2절 튜플 코드의 2번 줄에서 두 값의 차례만 바꿨습니다. 검사는 지나갈까요? 지나가지 못하면 몇 번 줄의 어디에서 막힐까요?

const row: [string, number] =
  [4500, '라테'];
힌트

1번 줄의 튜플이 자리마다 무슨 타입을 정했는지 보고, 2번 줄의 값을 자리마다 견줍니다.

정답
검사 결과 오류 2개
main.ts(2,4): error TS2322: Type 'number' is not assignable to type 'string'.main.ts(2,10): error TS2322: Type 'string' is not assignable to type 'number'.

2번 줄에서 두 곳이 막힙니다. 첫 자리 4500은 string 자리에, 둘째 자리 '라테'는 number 자리에 들지 않습니다. 튜플은 자리마다 타입을 보고, 검사기는 멈추지 않고 두 자리를 다 짚습니다.

흔한 오답은 '지나간다'입니다. 글자와 수가 하나씩 있으면 차례는 상관없다고 본 것입니다. 튜플의 타입은 자리의 차례까지 정합니다.

2. 검사를 지나도록 빈칸에 타입을 쓰세요. picked에는 고른 메뉴 이름을 모으고, row는 이름과 가격의 짝입니다.

const picked: ____ = [];
picked.push('라테');
const row: ____ =
  ['쿠키', 2000];
힌트

빈 배열에는 앞으로 넣을 원소의 타입을, 짝에는 자리마다의 타입을 적습니다.

정답

차례로 string[] · [string, number]입니다.

const picked: string[] = [];
picked.push('라테');
const row: [string, number] =
  ['쿠키', 2000];
console.log(picked, row);
검사 결과 오류 없음
실행 결과
[ '라테' ] [ '쿠키', 2000 ]

다르게 써도 되는 곳: row를 (string | number)[]로 적어도 검사는 지나지만, 자리마다의 타입과 길이를 잃습니다. any[]도 지나가지만 검사를 끄는 타입이라 쓰지 않습니다(6단원).

흔한 오답은 row의 string[]입니다. 2000이 string 원소 자리에 들지 않아 TS2322로 막힙니다.

3. 성적을 배열에 모으다가 셋째 점수를 따옴표 안에 적었습니다. 검사 결과와 Node로 돌린 실행 결과를 각각 쓰세요.

const scores = [90, 85];
scores.push('100');
const last = scores[2];
console.log(scores[0] + last);
힌트

검사는 2번 줄의 인자가 scores의 원소 타입 모음에 드는지로 봅니다. 실행은 검사 결과와 따로, 배열에 실제로 들어간 값으로 셈합니다.

정답
검사 결과 오류 1개
main.ts(2,13): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
실행 결과
90100

2번 줄이 막힙니다. number 원소 자리에 글자 '100'을 넣으려 했다는 뜻입니다. 3 · 4번 줄은 검사를 지납니다. last의 지금 타입이 원소 타입 number라서입니다. Node는 그래도 돌아 글자 '100'이 배열에 들어갑니다. 4번 줄은 90에 글자 '100'을 이어 붙여 90100을 찍습니다.

흔한 오답은 190입니다. 원소 타입이 number라서 '100'도 수로 바뀌어 들어간다고 본 것입니다. 타입은 실행 때 값을 바꾸지 않습니다(검사 규칙 1). '실행되지 않는다'는 답은 검사 오류가 있으면 Node도 돌지 않는다고 본 것입니다(검사 오류가 실행을 막는다는 생각).

컴퓨터에서 해 보기(선택) — 점수에 들지 않습니다. 폰으로만 보고 있다면 건너뛰세요. 1절 push 코드를 main.ts로 저장해 npx tsc로 검사하고 node main.ts로 돌립니다. 그다음 2번 줄의 '3500'을 3500으로 바꾸면 두 결과가 어떻게 바뀔지 먼저 적고, 다시 검사하고 돌려서 맞춰 봅니다. 검사하고 돌리는 법은 내 컴퓨터에서 검사하고 돌리기에 있습니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

타입스크립트 목차로