TYPESCRIPT · 심층 가이드
TypeScript 기초 문법 완전 정리
변수 선언과 분기, 반복, 배열과 맵, 타입 변환까지 TypeScript 기초 문법 20가지를 런타임 동작과 함께 짚어 봅니다.
TypeScript를 배울 때 가장 먼저 새겨야 할 사실은 타입이 실행 시점에는 남지 않는다는 것입니다. 컴파일이 끝나면 타입 표기는 전부 지워지고 순수한 JavaScript만 남습니다. 그래서 타입 검사는 작성 시점의 안전장치일 뿐, 외부에서 들어온 값이 선언한 타입과 실제로 일치하는지는 보장해 주지 않습니다. 이 경계를 흐리면 API 응답을 다룰 때 반드시 문제가 생깁니다.
숫자 변수 선언과 문자열 상수 선언에서 let과 const의 구분을 잡고, if else 분기와 switch 또는 match 사용, 삼항 연산자 사용으로 분기 표현을 정리하십시오. null 기본값 처리는 따로 떼어 볼 만한데, 널 병합 연산자와 논리 OR의 차이가 여기서 갈립니다. 뒤쪽의 정렬 기초와 타입 변환은 함께 읽으면 좋습니다.
정렬 기초에서 반드시 확인할 함정이 있습니다. 배열의 기본 정렬은 원소를 문자열로 바꿔서 비교합니다. 숫자 배열을 그냥 정렬하면 10이 9보다 앞에 오는 결과가 나옵니다. 숫자를 정렬할 때는 비교 함수를 직접 넘겨야 합니다. 같은 맥락에서 0.1 + 0.2가 정확히 0.3이 되지 않는 부동소수점 문제도 숫자 합계 계산에서 함께 짚고 넘어가십시오.
01숫자 변수 선언
count에 number를 명시하고, 5보다 큰지 비교한 결과로 label 문자열을 정합니다. 값과 타입을 함께 적어 두는 기본 선언 형태입니다. 리터럴로 초기화하면 타입이 추론되므로 : number는 생략해도 같고, 명시는 의도를 문서로 남기는 쪽에 가깝습니다.
TypeScript code
// 숫자 변수 선언
let count: number = 1;
const label: string = count > 5 ? "big" : "small";
console.log(label);리터럴로 초기화한 let은 number로, const는 1이라는 리터럴 타입으로 추론되어 유니온 타입 자리에 넣을 때 결과가 달라집니다.
초기화 없이 let count: number;로 선언한 뒤 값을 넣기 전에 읽으면, 타입은 숫자인데 실제 값은 undefined인 상태로 계산에 들어갑니다.
02문자열 상수 선언
for 문이 0부터 2까지 돌면서 i + 2를 출력해 2, 3, 4를 찍습니다. 상수 선언이 주제지만 코드는 반복 출력에 가깝습니다. let으로 선언한 i는 반복마다 새로 바인딩되어, 안에서 콜백을 만들어도 각자 다른 값을 기억합니다.
TypeScript code
// 문자열 상수 선언
for (let i = 0; i < 3; i += 1) {
console.log(i + 2);
}i += 1과 i++은 문장으로 쓸 때 결과가 같지만, 값을 함께 쓰는 표현식 안에서는 후위 증가가 증가 전 값을 돌려주어 차이가 납니다.
var로 바꾸면 바인딩이 하나뿐이라, 루프가 끝난 뒤 실행되는 콜백들이 전부 마지막 값 3을 보게 됩니다.
03불리언 플래그 만들기
숫자 배열을 스프레드로 복사한 뒤 (a, b) => a - b 비교 함수로 오름차순 정렬합니다. 원본을 건드리지 않고 정렬본만 필요할 때 이 형태를 씁니다. 비교 함수를 빼면 요소를 문자열로 바꿔 비교해서 10이 9보다 앞에 오는 결과가 나옵니다.
TypeScript code
// 불리언 플래그 만들기
const values: number[] = [3, 1, 4, 3];
console.log([...values].sort((a, b) => a - b));sort는 배열을 제자리에서 바꾸고 그 배열을 다시 돌려주므로, [...values]로 먼저 복사해야 원본이 처음 순서를 유지합니다.
비교 함수를 빼고 sort()만 부르면 요소가 문자열로 변환돼 비교되어, [3, 1, 4, 10]이 [1, 10, 3, 4] 순으로 나옵니다.
04문자열 길이 구하기
이름과 레벨을 가진 객체 리터럴을 만들고 두 속성을 꺼내 출력합니다. 주석 없이도 { name: string; level: number }로 추론됩니다. 구조적 타이핑이라 모양만 같으면 해당 타입 자리에 넣을 수 있지만, 리터럴을 직접 넘기면 없는 속성이 초과 속성 검사에 걸립니다.
TypeScript code
// 문자열 길이 구하기
const user = { name: "user4", level: 4 };
console.log(user.name, user.level);const user로 잡아도 속성 타입은 string과 number로 넓어집니다. 리터럴 값 그대로 고정하려면 as const를 붙여야 합니다.
이미 변수에 담은 객체는 초과 속성 검사를 우회하므로, 여분의 필드가 섞인 값이 그대로 함수에 넘어가 오타 난 키를 잡지 못합니다.
05문자열 정리하기
count가 5라서 count > 5는 거짓이 되고 label에는 작은 쪽 문자열이 들어갑니다. 문자열을 다듬는 주제와 달리 실제로는 비교 결과로 문자열을 고릅니다. 공백 정리에는 trim()을 쓰되, 원본은 그대로 남고 새 문자열이 반환됩니다.
TypeScript code
// 문자열 정리하기
let count: number = 5;
const label: string = count > 5 ? "big" : "small";
console.log(label);trim()은 새 문자열을 돌려주므로 반환값을 다시 대입하지 않으면 아무 효과가 없고, 양끝만 다듬을 뿐 가운데 공백은 그대로 남습니다.
빈 값 검사를 if (value)로만 하면 공백만 들어 있는 ' '가 참으로 통과해, 이름 없는 데이터가 그대로 저장됩니다.
06숫자 합계 계산
0, 1, 2에 6을 더한 값을 차례로 출력합니다. 합계를 구하려면 루프 밖에 누적 변수를 두고 매 회차 더해야 하는데, 이 코드는 출력만 합니다. 배열이 있다면 reduce 한 줄로 같은 일을 하며, 초기값을 주지 않으면 빈 배열에서 예외가 납니다.
TypeScript code
// 숫자 합계 계산
for (let i = 0; i < 3; i += 1) {
console.log(i + 6);
}누적 변수를 루프 밖에 두는 방식과 reduce는 결과가 같지만, reduce는 중간 값이 밖으로 새지 않아 재사용 실수를 막아 줍니다.
reduce에 초깃값을 주지 않으면 첫 요소가 누산기가 되고 빈 배열에서는 TypeError가 던져집니다. 합계에는 0을 명시해야 합니다.
07나머지 연산 확인
값 네 개짜리 배열을 복사해 정렬한 결과를 보여 줍니다. 나머지 연산을 확인하려면 %가 필요하지만 코드에는 정렬만 있습니다. %는 왼쪽 피연산자의 부호를 따라가서 -7 % 3이 -1이 되므로, 순환 인덱스 계산에는 그대로 쓰기 어렵습니다.
TypeScript code
// 나머지 연산 확인
const values: number[] = [3, 1, 4, 7];
console.log([...values].sort((a, b) => a - b));순환 인덱스가 필요하면 ((i % n) + n) % n처럼 한 번 더 보정해야 음수 입력에서도 0 이상인 값이 나옵니다.
홀수 판정을 x % 2 === 1로 쓰면 -3이 -1을 돌려주어 홀수인데도 거짓이 됩니다. !== 0으로 비교해야 합니다.
08if else 분기
이름과 레벨 8을 담은 객체를 만들어 값을 출력합니다. 분기 대신 데이터 구성이 전부이며, 조건을 넣는다면 level 값을 if로 비교하는 자리가 됩니다. 이렇게 리터럴로 만든 객체의 속성은 존재가 보장되어 strictNullChecks에서도 추가 검사가 필요 없습니다.
TypeScript code
// if else 분기
const user = { name: "user8", level: 8 };
console.log(user.name, user.level);선택 속성으로 선언한 필드는 값이 undefined일 수 있어 접근 전 확인이 필요하고, exactOptionalPropertyTypes를 켜면 명시적 undefined 대입도 막힙니다.
객체에 없는 키를 any로 단언해 읽으면 검사 없이 undefined가 흘러가, 그 값을 쓰는 다음 연산에서 NaN이 됩니다.
09switch 또는 match 사용
count가 9이므로 삼항 비교가 참이 되어 큰 쪽 문자열이 출력됩니다. 갈래가 둘뿐이면 switch보다 이 편이 짧습니다. 유니온 타입을 switch로 나눌 때는 각 case에서 타입이 좁혀지고, never를 이용해 빠진 갈래를 컴파일 단계에서 잡을 수 있습니다.
TypeScript code
// switch 또는 match 사용
let count: number = 9;
const label: string = count > 5 ? "big" : "small";
console.log(label);유니온을 switch로 나눌 때 default에서 never 타입 변수에 대입해 두면, 새 갈래를 추가한 순간 그 자리에서 컴파일 오류가 납니다.
case 끝에 break를 빠뜨리면 다음 갈래로 그대로 흘러 들어가, 조건에 맞지 않는 처리까지 함께 실행됩니다.
10null 기본값 처리
루프가 10, 11, 12를 출력합니다. 기본값 처리를 붙인다면 값이 없을 때를 대비해 ??를 쓰는 자리가 됩니다. ??는 null과 undefined에만 반응해서, 0이나 빈 문자열을 살려야 할 때 ||보다 안전합니다.
TypeScript code
// null 기본값 처리
for (let i = 0; i < 3; i += 1) {
console.log(i + 10);
}??는 ||나 &&와 괄호 없이 섞어 쓸 수 없도록 문법으로 막혀 있어, 우선순위를 착각한 코드가 애초에 컴파일되지 않습니다.
기본값을 count || 10으로 쓰면 실제 값 0까지 10으로 바뀝니다. 0을 살려야 한다면 ??를 써야 합니다.
11삼항 연산자 사용
정렬 코드가 배열 [3, 1, 4, 11]을 오름차순으로 만들어 보여 줍니다. 삼항 연산자는 이 안의 비교 함수를 a < b ? -1 : 1 형태로 바꿔 쓸 때 등장합니다. 다만 뺄셈 방식과 달리 동치일 때 0을 돌려주지 않으면 같은 값끼리 순서가 뒤집힐 수 있습니다.
TypeScript code
// 삼항 연산자 사용
const values: number[] = [3, 1, 4, 11];
console.log([...values].sort((a, b) => a - b));비교 함수는 음수, 0, 양수만 구분하므로 a < b ? -1 : a > b ? 1 : 0처럼 세 갈래를 모두 돌려주는 형태가 안전합니다.
a - b 방식은 아주 큰 정수에서 뺄셈이 정밀도를 잃어 순서가 뒤틀릴 수 있으므로, 그런 값에는 대소 비교로 바꿔야 합니다.
12for 반복문
레벨 12를 가진 객체를 만들고 두 필드를 출력합니다. 반복문이 주제이지만 코드에는 루프가 없고, 객체를 순회하려면 Object.entries로 키와 값을 꺼내야 합니다. 이때 키 타입은 string으로 넓어져, 원래 키 이름을 유지하려면 별도 단언이 필요합니다.
TypeScript code
// for 반복문
const user = { name: "user12", level: 12 };
console.log(user.name, user.level);Object.entries는 자기 자신의 열거 가능한 문자열 키만 돌려주므로, 프로토타입에서 물려받은 속성이나 심벌 키는 순회에서 빠집니다.
키 이름을 유지하겠다고 as keyof typeof user로 단언하면, 실제로는 없는 키가 들어와도 검사되지 않아 undefined가 조용히 흘러갑니다.
13while 누적 계산
13은 5보다 크므로 label에 큰 쪽 문자열이 들어갑니다. while로 누적하려면 조건과 증가를 직접 관리해야 하지만 여기서는 비교 한 번으로 끝납니다. let 변수는 재할당이 가능하고, const는 재할당만 막을 뿐 객체 내부 변경은 막지 못합니다.
TypeScript code
// while 누적 계산
let count: number = 13;
const label: string = count > 5 ? "big" : "small";
console.log(label);const로 잡은 객체도 속성 변경은 가능하므로, 값 자체를 얼리려면 Object.freeze나 readonly 표기가 따로 필요합니다.
누적 변수를 const로 선언하면 대입 시점에 컴파일 오류가 나고, 반대로 조건에 쓰는 변수를 갱신하지 않으면 반복이 끝나지 않습니다.
14break 와 continue
0부터 2까지 돌며 14, 15, 16을 출력합니다. break나 continue는 없으므로 세 번 모두 실행됩니다. 두 키워드는 가장 가까운 루프에만 작용하고, 중첩 루프를 한 번에 빠져나오려면 루프에 레이블을 붙여야 합니다.
TypeScript code
// break 와 continue
for (let i = 0; i < 3; i += 1) {
console.log(i + 14);
}break는 switch 안에서도 쓰이므로, switch를 감싼 루프를 빠져나오려면 레이블을 붙이거나 플래그를 따로 둬야 합니다.
forEach 안에서는 break를 쓸 수 없고 return은 한 항목만 건너뛰므로, 중간에 멈춰야 한다면 for...of로 바꿔야 합니다.
15배열 생성과 순회
배열을 [...values]로 복제한 뒤 정렬해 출력합니다. 순회 자체는 없지만, 복사본을 만드는 습관은 원본 공유로 생기는 버그를 줄여 줍니다. 스프레드 복사는 얕은 복사라 요소가 객체이면 안쪽은 여전히 같은 참조를 가리킵니다.
TypeScript code
// 배열 생성과 순회
const values: number[] = [3, 1, 4, 15];
console.log([...values].sort((a, b) => a - b));요소가 객체인 배열을 진짜로 분리하려면 각 요소까지 새로 만들어야 하며, structuredClone은 함수를 담은 값에서는 예외를 던집니다.
const copy = values로 대입한 뒤 정렬하면 같은 배열을 가리키므로 원본 순서까지 바뀌어, 그 배열을 쓰던 다른 코드가 어긋납니다.
16맵 또는 객체 생성
키 두 개짜리 객체를 만들어 점 표기법으로 읽습니다. 키가 고정되어 있으면 이런 객체로 충분하고, 키가 실행 중에 늘어난다면 Map이 낫습니다. Map은 문자열이 아닌 키도 쓸 수 있고 삽입 순서를 지키며 size로 개수를 바로 얻습니다.
TypeScript code
// 맵 또는 객체 생성
const user = { name: "user16", level: 16 };
console.log(user.name, user.level);Map은 size로 개수를 바로 얻고 삽입 순서를 지키지만, JSON.stringify에 넘기면 내용이 사라지고 {}만 남습니다.
객체를 사전처럼 쓰면서 user['toString']처럼 상속된 키를 조회하면 값이 없는데도 함수가 잡혀, 존재 검사가 틀리게 통과합니다.
17정렬 기초
17과 5를 비교해 큰 쪽 라벨을 고르는 코드로, 정렬 알고리즘은 나오지 않습니다. 정렬의 핵심은 이런 비교를 어떤 규칙으로 반복하느냐에 있습니다. 문자열을 한글이나 악센트까지 고려해 정렬하려면 localeCompare를 비교 함수로 넘겨야 합니다.
TypeScript code
// 정렬 기초
let count: number = 17;
const label: string = count > 5 ? "big" : "small";
console.log(label);localeCompare는 옵션을 받을 수 있어 { numeric: true }를 주면 item2가 item10보다 앞에 오도록 정렬됩니다.
비교 함수 안에서 localeCompare를 매번 부르면 목록이 클 때 비용이 커지므로, Intl.Collator 인스턴스를 만들어 재사용해야 합니다.
18튜플 또는 페어 사용
루프가 18, 19, 20을 출력합니다. 튜플이 주제지만 코드에는 없고, 두 값을 묶으려면 [string, number]처럼 길이와 위치별 타입을 고정한 배열 타입을 씁니다. 일반 배열과 달리 튜플은 인덱스마다 타입이 달라 잘못된 위치 접근이 컴파일에서 걸립니다.
TypeScript code
// 튜플 또는 페어 사용
for (let i = 0; i < 3; i += 1) {
console.log(i + 18);
}튜플에도 [string, number?]처럼 선택 요소나 나머지 요소를 쓸 수 있고, as const를 붙이면 읽기 전용 튜플로 고정됩니다.
튜플을 push로 늘리는 것은 막히지 않아 길이가 실제로 늘어나며, 그 뒤의 인덱스 접근은 타입상 존재하지 않는 자리로 취급됩니다.
19타입 변환
숫자 배열을 정렬해 출력하며, 요소는 이미 number[]로 선언되어 변환이 필요 없습니다. 문자열을 숫자로 바꿔야 한다면 Number()를 거치고 결과가 NaN인지 확인합니다. as 단언은 컴파일 단계의 표시일 뿐 값을 바꾸지 않아, 잘못 쓰면 런타임에서 어긋납니다.
TypeScript code
// 타입 변환
const values: number[] = [3, 1, 4, 19];
console.log([...values].sort((a, b) => a - b));Number('')는 0, Number(' 12 ')는 12를 돌려주므로, 사용자 입력에는 빈 문자열 검사와 Number.isNaN 확인을 함께 걸어야 합니다.
as로 문자열을 숫자라고 단언해도 값은 그대로 문자열이라, 뒤이은 덧셈이 숫자 합이 아니라 문자열 연결로 처리됩니다.
20날짜 문자열 만들기
객체 하나를 만들어 이름과 레벨을 출력할 뿐, 날짜 처리는 들어 있지 않습니다. 날짜를 문자열로 만들 때는 toISOString()이 가장 예측 가능한 결과를 줍니다. 이 값은 항상 UTC 기준이라 로컬 시간대와 하루가 어긋나 보일 수 있습니다.
TypeScript code
// 날짜 문자열 만들기
const user = { name: "user20", level: 20 };
console.log(user.name, user.level);toISOString()은 언제나 UTC 기준으로 Z가 붙은 문자열을 주고, 로컬 시간대를 반영한 표기가 필요하면 Intl.DateTimeFormat을 써야 합니다.
new Date('2026-08-30')은 UTC 자정으로 해석되어, 서쪽 시간대에서 날짜만 꺼내면 의도한 날보다 하루 전으로 보입니다.
정리하며
- 타입은 컴파일 후 지워지므로 외부 입력의 실제 형태는 따로 검증합니다
- 널 병합 연산자와 논리 OR은 0과 빈 문자열에서 결과가 갈립니다
- 기본 정렬은 문자열 비교라 숫자 배열에는 비교 함수를 넘겨야 합니다
- 부동소수점 합계는 오차가 생기므로 비교 시 허용 범위를 둡니다
더 깊이 들어가고 싶다면 TypeScript 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.