타입스크립트 기초 강좌 #2 기본 타입

15 분 소요

지난 시간에는 타입스크립트가 무엇이고 왜 쓰는지, 첫 코드를 컴파일해 실행하는 곳까지 다뤘습니다. 이번에는 매일 쓰게 될 기본 타입들을 정리합니다.

원시 타입 — string / number / boolean

자바스크립트의 원시 타입과 일대일로 매칭됩니다.

원시 타입
const name: string = '철수';
const age: number = 30;
const isAdmin: boolean = false;

타입스크립트의 string, number, boolean모두 소문자입니다. 자바의 String/Integer 같은 대문자 클래스 타입과 헷갈리지 마세요 — 타입스크립트의 String(대문자)도 존재하긴 하지만 거의 쓸 일이 없고 일반적으로는 소문자 string을 씁니다.

number는 정수와 실수 모두 포함

자바스크립트와 동일하게 number는 정수와 실수를 구분하지 않습니다.

number
const count: number = 42;
const pi: number = 3.14;
const big: number = 1_000_000;     // 자릿수 구분 (자바스크립트 문법)
const hex: number = 0xff;          // 16진수
const bin: number = 0b1010;        // 2진수

큰 정수가 필요하면 bigint 타입(100n)을 쓸 수도 있는데 일상적으로는 거의 안 쓰입니다.

null과 undefined

자바스크립트에 두 가지 "값이 없음"이 있죠. 타입스크립트도 그대로 따라갑니다.

null과 undefined
const a: null = null;
const b: undefined = undefined;

이 두 타입은 단독으로는 거의 안 쓰이고, 보통 다른 타입과 union(#4)으로 결합해 사용합니다.

null이거나 string
let user: string | null = null;
user = '철수';

배열

배열의 타입은 두 가지 표기법이 있습니다.

배열 타입 - 두 표기법
const fruits: string[] = ['사과', '바나나', '체리'];
const numbers: Array<number> = [1, 2, 3, 4];

string[]Array<string>완전히 같은 의미입니다. 더 짧은 [] 형태가 일반적이고, 제네릭 형태는 좀 복잡한 타입을 표현할 때 가독성이 좋아 가끔 쓰입니다.

배열 안에 다른 타입을 넣으려고 하면 막힙니다.

잘못된 사용
const fruits: string[] = ['사과', 42];  // 🚫 number는 string[]에 못 들어감

여러 타입을 섞고 싶다면 union(#4)으로:

혼합 배열
const mixed: (string | number)[] = ['사과', 42, '바나나'];

튜플 (tuple)

배열의 일종이지만 각 위치마다 정해진 타입이 있는 형태입니다.

튜플
let point: [number, number] = [10, 20];
let labeled: [string, number] = ['age', 30];
 
point[0];  // number
point[1];  // number
 
labeled[0];  // string
labeled[1];  // number
labeled[2];  // 🚫 에러: 인덱스 2는 정의되지 않음

길이도 고정되고 각 위치의 타입도 고정됩니다. 좌표 (x, y)나 (key, value) 페어 같이 자리마다 의미가 다른 데이터에 잘 맞아요.

자바스크립트의 함수가 여러 값을 반환할 때 자주 쓰입니다.

useState 같은 패턴
function useCounter(initial: number): [number, () => void] {
  // ... (간략화)
  return [42, () => {}];
}
 
const [count, increment] = useCounter(0);

리액트의 useState도 정확히 이런 튜플 반환 패턴이에요.

객체 타입

객체의 모양도 명시할 수 있습니다.

객체 타입
const user: { name: string; age: number; isAdmin: boolean } = {
  name: '철수',
  age: 30,
  isAdmin: false,
};

이 인라인 객체 타입이 길어지면 가독성이 떨어집니다. 그래서 **interface**나 **type**으로 별도 정의하는 게 일반적인데, 이건 #3에서 자세히 다룹니다. 여기서는 인라인 형태만 알아두면 충분해요.

옵셔널 프로퍼티

물음표를 붙이면 그 프로퍼티가 없어도 OK입니다.

옵셔널
const user: { name: string; age?: number } = { name: '철수' };  // age 없어도 OK

readonly

읽기 전용 프로퍼티는 readonly를 붙입니다.

readonly
const user: { readonly id: string; name: string } = { id: 'u-1', name: '철수' };
 
user.name = '영희';  // ✓
user.id = 'u-2';     // 🚫 readonly라서 변경 불가

readonly는 컴파일 시점의 보호일 뿐 자바스크립트 수준의 진짜 readonly(Object.freeze)는 아닙니다. 그래도 의도치 않은 수정을 잡는 데 매우 유용해요.

enum

여러 이름이 붙은 상수의 집합입니다.

enum
enum Color {
  Red,
  Green,
  Blue,
}
 
const c: Color = Color.Red;
console.log(c);  // 0

enum 멤버는 기본적으로 0부터 시작하는 정수가 자동 할당됩니다. 명시적으로 값을 지정할 수도 있고요.

문자열 enum
enum Status {
  Pending = 'PENDING',
  Active = 'ACTIVE',
  Deleted = 'DELETED',
}
 
const s: Status = Status.Active;  // 'ACTIVE'

enum의 대안 — Union of literals

타입스크립트 커뮤니티에서는 enum 대신 literal union(#4)을 더 선호하는 경향이 있습니다.

enum 대신 literal union
type Status = 'pending' | 'active' | 'deleted';
 
const s: Status = 'active';

이게 더 가벼우면서 컴파일 결과도 더 단순합니다 (enum은 컴파일 시 추가 객체를 생성해요). #4에서 자세히 다룹니다.

이 시리즈에서는 enum도 알아두되, 새 코드에서는 literal union을 우선 고려하는 자세를 권합니다.

any — 타입 검사 끄기

any 타입은 "어떤 타입이든 허용"입니다.

any
let value: any = '문자열';
value = 42;
value = { name: '철수' };
value.foo.bar.baz;  // 🚫 런타임 에러 가능 — 컴파일은 통과

any로 선언하면 타입스크립트가 그 변수에 대한 모든 검사를 멈춥니다. 무엇이든 들어갈 수 있고, 무엇이든 호출할 수 있고, 결과적으로 자바스크립트와 똑같이 동작해요.

any타입스크립트의 탈출구입니다. 가끔 어쩔 수 없을 때만 쓰고, 일상적으로는 피하는 게 좋습니다. 너무 자주 쓰면 타입스크립트를 쓰는 의미가 없어져요.

unknown — 안전한 any

any와 비슷하게 "어떤 값이든 들어올 수 있음"을 의미하지만, 사용할 때 타입 검사를 강제하는 더 안전한 타입입니다.

unknown
let value: unknown = '문자열';
 
value.toUpperCase();  // 🚫 unknown이라 메소드 호출 못 함
 
if (typeof value === 'string') {
  value.toUpperCase();  // ✓ 분기 안에서 string으로 좁혀짐
}

외부 API 응답이나 사용자 입력처럼 타입을 모르는 값을 다룰 때 any 대신 unknown을 쓰면 안전합니다. 사용 전에 항상 타입 좁히기(narrowing, #4)를 강제하니까요.

void — 반환값 없음

함수가 의미 있는 값을 반환하지 않을 때 사용합니다.

void
function log(message: string): void {
  console.log(message);
}

voidundefined와 비슷하지만 의미가 다릅니다. undefined명시적으로 undefined를 반환한다는 뜻이고, void반환값에 관심 없음입니다. 콜백 함수의 반환 타입에 자주 쓰여요.

never — 절대 일어나지 않음

함수가 항상 throw하거나 무한 루프라서 정상적으로 반환되지 않을 때 쓰는 타입입니다.

never
function fail(message: string): never {
  throw new Error(message);
}
 
function infiniteLoop(): never {
  while (true) {
    // ...
  }
}

평소엔 직접 적을 일이 거의 없고, 타입 좁히기에서 "이 분기는 도달 불가능"임을 표현할 때 가끔 등장합니다.

타입 단언 (type assertion)

타입스크립트가 모르는 정보를 우리가 더 잘 알 때, **"이건 이 타입이야"**라고 단언할 수 있습니다.

타입 단언
const input = document.getElementById('email') as HTMLInputElement;
input.value;  // ✓ HTMLInputElement에는 value가 있음

document.getElementById는 기본적으로 HTMLElement | null을 반환하는데, 우리는 그게 input 태그라는 걸 알고 있으니 단언해서 좁힙니다.

as 타입 형태가 표준이고, 옛날 스타일인 <HTMLInputElement>...도 있지만 JSX와 충돌이 있어 거의 안 씁니다.

타입 단언은 타입 검사를 부분적으로 우회합니다. any만큼 위험하진 않지만, 잘못 쓰면 런타임 에러로 이어질 수 있어요. 진짜로 우리가 더 잘 안다는 확신이 있을 때만 써야 합니다.

가독성 잘 잡힌 예시

지금까지 다룬 것들을 종합한 작은 예시.

실전 같은 코드
// 사용자 데이터 모양
const user: {
  readonly id: string;
  name: string;
  email: string;
  age?: number;
  roles: string[];
} = {
  id: 'u-1',
  name: '철수',
  email: 'cheolsu@example.com',
  roles: ['admin', 'editor'],
};
 
// 검색 결과 — 좌표와 거리 튜플
const result: [number, number, number] = [37.5, 127.0, 5.2];
const [lat, lng, distance] = result;
 
// 함수 — void 반환
function logUser(u: typeof user): void {
  console.log(`${u.name} (${u.email})`);
}
 
// 알 수 없는 외부 데이터
function processApiResponse(data: unknown): void {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    console.log('이름:', (data as { name: string }).name);
  }
}

객체 타입이 인라인이라 user 부분이 좀 길죠. 다음 글(#3)에서 interfacetype으로 이걸 깔끔하게 분리하는 방법을 다룹니다.

직접 해보기

index.ts에 다음을 작성하고 컴파일해보세요. 의도적으로 몇 군데 에러를 두었습니다.

index.ts (실수 찾기)
let name: string = '철수';
name = 42;  // 1번 에러?
 
const ages: number[] = [30, '서른', 28];  // 2번 에러?
 
const point: [number, number] = [10, 20, 30];  // 3번 에러?
 
const user: { id: string; age?: number } = {
  id: 'u-1',
  age: undefined,  // 이건 OK일까?
};
 
let value: unknown = '문자열';
console.log(value.toUpperCase());  // 4번 에러?

npx tsc로 컴파일하면서 어떤 줄에 어떤 에러가 나는지 직접 확인하면 머릿속에 잘 자리잡습니다. 답을 미리 보지 말고 본인이 추측한 다음 확인해보세요.

마무리

이번 글에서는 일상적으로 쓰게 될 기본 타입들을 한 번에 훑었습니다.

  • 원시: string, number, boolean, null, undefined
  • 컬렉션: T[], Array<T>, 튜플 [T1, T2]
  • 객체: 인라인 { ... } (옵셔널 ?, readonly)
  • 묶음: enum + literal union 대안
  • 특수: any (탈출구), unknown (안전한 any), void, never
  • 단언: value as 타입

객체 타입이 길어지는 문제는 다음 글에서 풉니다. "타입스크립트 기초 강좌 #3 interface와 type alias"에서는 객체와 함수의 타입 모양을 이름 붙여 재사용하는 두 도구 — **interface**와 **type alias**를 다룹니다. 두 도구의 차이와 언제 어느 쪽을 쓰는지가 자주 헷갈리는 주제라 한 글로 정리할 거예요.