지난 시간에는 타입스크립트가 무엇이고 왜 쓰는지, 첫 코드를 컴파일해 실행하는 곳까지 다뤘습니다. 이번에는 매일 쓰게 될 기본 타입들을 정리합니다.
원시 타입 — string / number / boolean
자바스크립트의 원시 타입과 일대일로 매칭됩니다.
const name: string = '철수';
const age: number = 30;
const isAdmin: boolean = false;타입스크립트의 string, number, boolean은 모두 소문자입니다. 자바의 String/Integer 같은 대문자 클래스 타입과 헷갈리지 마세요 — 타입스크립트의 String(대문자)도 존재하긴 하지만 거의 쓸 일이 없고 일반적으로는 소문자 string을 씁니다.
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
자바스크립트에 두 가지 "값이 없음"이 있죠. 타입스크립트도 그대로 따라갑니다.
const a: null = null;
const b: undefined = undefined;이 두 타입은 단독으로는 거의 안 쓰이고, 보통 다른 타입과 union(#4)으로 결합해 사용합니다.
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) 페어 같이 자리마다 의미가 다른 데이터에 잘 맞아요.
자바스크립트의 함수가 여러 값을 반환할 때 자주 쓰입니다.
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 없어도 OKreadonly
읽기 전용 프로퍼티는 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 Color {
Red,
Green,
Blue,
}
const c: Color = Color.Red;
console.log(c); // 0enum 멤버는 기본적으로 0부터 시작하는 정수가 자동 할당됩니다. 명시적으로 값을 지정할 수도 있고요.
enum Status {
Pending = 'PENDING',
Active = 'ACTIVE',
Deleted = 'DELETED',
}
const s: Status = Status.Active; // 'ACTIVE'enum의 대안 — Union of literals
타입스크립트 커뮤니티에서는 enum 대신 literal union(#4)을 더 선호하는 경향이 있습니다.
type Status = 'pending' | 'active' | 'deleted';
const s: Status = 'active';이게 더 가벼우면서 컴파일 결과도 더 단순합니다 (enum은 컴파일 시 추가 객체를 생성해요). #4에서 자세히 다룹니다.
이 시리즈에서는 enum도 알아두되, 새 코드에서는 literal union을 우선 고려하는 자세를 권합니다.
any — 타입 검사 끄기
any 타입은 "어떤 타입이든 허용"입니다.
let value: any = '문자열';
value = 42;
value = { name: '철수' };
value.foo.bar.baz; // 🚫 런타임 에러 가능 — 컴파일은 통과any로 선언하면 타입스크립트가 그 변수에 대한 모든 검사를 멈춥니다. 무엇이든 들어갈 수 있고, 무엇이든 호출할 수 있고, 결과적으로 자바스크립트와 똑같이 동작해요.
any는 타입스크립트의 탈출구입니다. 가끔 어쩔 수 없을 때만 쓰고, 일상적으로는 피하는 게 좋습니다. 너무 자주 쓰면 타입스크립트를 쓰는 의미가 없어져요.
unknown — 안전한 any
any와 비슷하게 "어떤 값이든 들어올 수 있음"을 의미하지만, 사용할 때 타입 검사를 강제하는 더 안전한 타입입니다.
let value: unknown = '문자열';
value.toUpperCase(); // 🚫 unknown이라 메소드 호출 못 함
if (typeof value === 'string') {
value.toUpperCase(); // ✓ 분기 안에서 string으로 좁혀짐
}외부 API 응답이나 사용자 입력처럼 타입을 모르는 값을 다룰 때 any 대신 unknown을 쓰면 안전합니다. 사용 전에 항상 타입 좁히기(narrowing, #4)를 강제하니까요.
void — 반환값 없음
함수가 의미 있는 값을 반환하지 않을 때 사용합니다.
function log(message: string): void {
console.log(message);
}void는 undefined와 비슷하지만 의미가 다릅니다. undefined는 명시적으로 undefined를 반환한다는 뜻이고, void는 반환값에 관심 없음입니다. 콜백 함수의 반환 타입에 자주 쓰여요.
never — 절대 일어나지 않음
함수가 항상 throw하거나 무한 루프라서 정상적으로 반환되지 않을 때 쓰는 타입입니다.
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)에서 interface와 type으로 이걸 깔끔하게 분리하는 방법을 다룹니다.
직접 해보기
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**를 다룹니다. 두 도구의 차이와 언제 어느 쪽을 쓰는지가 자주 헷갈리는 주제라 한 글로 정리할 거예요.