
안녕하세요. GDGoC Gachon에서 활동 중인 최선규입니다. 이번에는 Typescript를 하고싶은 분들을 위해 가장 기초가 되는 Type에 대해서 간단히 설명해보고자 합니다. 글의 반응을보고 다들 좋아해주시면, “집합론”의 관점과 더불어 타입을 사용함에 있어 아주 중요한 개념들을 더 다루어보고자 하니 많은 관심 부탁드릴게요. ㅎㅎ
우선 Typescript는 타입만 가지고도 알고리즘 구현이 가능할 정도로 높은 표현력을 지닌 언어입니다. 그럼에도 불구하고 몇가지 제약 때문에 모든 타입을 표현하는 것은 불가능하죠. 대부분의 경우 표현력 자체의 문제라기보단 너무 많은 연산을 사용하는 것을 방지하기 위한 제한에 가깝습니다. 그러니 지금은 제한 사항은 잠시 미루어놓고 이런 풍부한 표현력을 활용하기 위한 기초를 다져봅시다.
프로그래밍에서 타입(type) 이라는 개념은 “값이 어떤 형태를 가지고 있고, 어떤 연산이 가능한가”를 정해주는 규칙입니다.
예를 들어, 우리가 3 + 5라는 연산을 하면 두 값이 모두 숫자이기 때문에 더하기가 가능합니다. 하지만 "3" + 5라는 코드를 보게 되면 어떻게 될까요? JavaScript에서는 문자열과 숫자가 만나 문자열 "35"로 변환됩니다. 동적 타입 언어이기 때문에 런타임(실행 시점)에 규칙이 적용되는 것이죠.
TypeScript는 다릅니다. 코드를 실행하기 전에 “이 값이 어떤 타입인지”를 검사해 줍니다. 그래서 잘못된 조합을 미리 잡아낼 수 있습니다.
// TypeScript 예시
function add(a: number, b: number) {
return a + b;
}
add(1, 2); // OK
add("1", 2); // ❌ Error: 'string' 형식은 'number' 형식에 할당할 수 없습니다.
이처럼 TypeScript는 정적 타입 시스템을 통해 우리가 의도하지 않은 동작을 예방합니다. 단순히 에러를 막아주는 것뿐만 아니라, 코드가 “이 값은 무슨 역할을 한다”는 걸 더 명확하게 설명할 수 있습니다.
예를 들어, 아래처럼 함수의 매개변수에 타입을 지정해 주면 IDE(코드 에디터)에서 자동완성, 빠른 에러 감지, 문서화 효과까지 얻을 수 있습니다.
function greet(name: string) {
return `Hello, ${name}`;
}
greet("Luke"); // 자동완성, 타입 검사 모두 OK
greet(123); // ❌ Error
결국 타입을 잘 사용하면 우리가 원하는 방향대로 코드를 더 잘 이끌어나갈 수 있게 되는 것이죠.
대부분의 강 타입 언어들은 아래의 원시형 타입들을 변수에 미리 지정해주는 것으로 프로그래밍을 시작합니다. Typescript에서는 지정하지 않는 경우에는 any로 명시될 뿐, 다른 강타입 언어들과 동일하게 동작합니다. 즉, Javascript에서는 어떠한 타입도 지엉되지 않았음으로 모든게 any인 Typescript라고 할 수 있겠네요.
우리가 흔히 아는 기본 타입들 입니다. 다른 프로그래밍 언어와의 차이점이라면 정수와 실수형 타입을 따로 분류하지 않는다는 점 정도일까요? 무튼, Typescript에서는 number string boolean을 통틀어 원시 타입이라 부릅니다.
let age: number = 29;
let pi: number = 3.14;
let name: string = "Luke";
let greeting: string = `Hello, ${name}`;
let isActive: boolean = true;
number → 정수, 실수 모두 포함string → 큰따옴표, 작은따옴표, 백틱(`) 모두 가능boolean → true/false 값만 허용[래퍼 객체와 원시 타입]
JavaScript에는
string(원시)과String(래퍼 객체)처럼 비슷해 보이지만 다른 타입이 있습니다. 하지만, Typescript에서는 래퍼 객체 사용을 지양하는 것이 좋습니다. 앞으로 자주 사용하게 될typeof키워드는 값 레벨에서나 타입 레벨에서나 래퍼 객체를 가르킬 일이 없기 때문이죠.let s1: string = "hello"; // 원시 타입 let s2: String = new String("hi"); // 객체 래퍼 console.log(typeof s1); // "string" console.log(typeof s2); // "object" - 레퍼 객체를 위한 "String"이라는 타입은 없음
리터럴 타입은 값 자체를 타입으로 사용하는 것입니다.
즉, "yes"라는 문자열 값이 동시에 타입이 될 수 있습니다.
let direction: "left" | "right";
direction = "left"; // OK
direction = "right"; // OK
// direction = "up"; // ❌ Error
숫자나 불리언도 마찬가지입니다.
let count: 0 | 1 | 2;
count = 1; // OK
// count = 3; // ❌ Error
let isOn: true; // 오직 true만 가능
isOn = true;
// isOn = false; // ❌ Error
리터럴 타입은 제한된 값 집합을 정의할 때 유용합니다. 주로 유니온 타입과 함께 Enum 대체로 많이 사용합니다.
Typescript에는 값의 nullable을 나타내는 두 가지 타입이 있습니다. 바로 null과 undefined 이죠. null은 명시적으로 값이 없음을 나타내는 객체 타입이고, undefined는 변수에 값이 할당되지 않았을 때 자동으로 할당되는 값입니다.
let a;
console.log(a); // undefined, 변수를 선언만 하고 값을 할당하지 않음
let b = null;
console.log(b); // null, 변수에 명시적으로 null을 할당
console.log(typeof a); // undefined, a는 undefined 값
console.log(typeof b); // object, b는 null 객체
[**strict null checks]**
Typescript에는 strict null checks(
strictNullChecks)라는 옵션이 존재합니다. 해당 옵션을 통해null과undefined를 명시적으로 타입에 포함하거나 제외할 수 있습니다. 다시 말해 이 옵션을 활성화하면null과undefined는 다른 타입으로 구분되고, 변수에 값이 없음을 나타낼 때 더 정확하게 되죠.
TypeScript에서는 객체(Object), 배열(Array), 튜플(Tuple)을 모두 타입 시스템 안에서 명확하게 표현할 수 있습니다. 이 세 가지는 자주 등장하는 데이터 구조이므로, 차이를 이해하는 것이 중요합니다.
객체는 키–값 쌍의 모음으로, 각 속성의 타입을 직접 지정할 수 있습니다.
const user: { id: number; name: string } = {
id: 1,
name: "Luke",
};
각 속성의 타입을 명시적으로 지정 가능하며, 필요하다면 선택적 속성(?)도 정의할 수 있습니다.
interface User {
id: number;
name: string;
email?: string; // 선택적 속성
}
const u1: User = { id: 1, name: "Alice" };
const u2: User = { id: 2, name: "Bob", email: "bob@test.com" };
[인덱스 시그니처]
객체의 키가 동적으로 결정되는 경우는 인덱스 시그니처를 활용합니다.
interface StringMap { [key: string]: string; // 인덱스 시그니처 } const colors: StringMap = { red: "#ff0000", green: "#00ff00", };
TypeScript에는 가변 길이의 Array와 고정 길이의 Tuple이 있습니다.
[정의의 차이]
[타입 시스템의 차이]
Array:
Array<T> 또는 T[] 형식으로 정의됩니다. 여기서 numbers는 오직 number 타입만 허용되죠. 즉, numbers[0]은 number 타입이며, numbers[1]도 number 타입이어야 합니다.const numbers: number[] = [1, 2, 3];
const strings: Array<string> = ["a", "b", "c"];
Tuple:
[타입1, 타입2, ...] 형식으로 정의합니다.// 첫 번째 요소는 string, 두 번째 요소는 number
const person: [string, number] = ["John", 30];
[인덱스 및 크기]
let tuple: [string, number] = ["John", 30];
tuple = ["Alice", 25]; // ✅ 정상
tuple = ["Alice", "25"]; // ❌ 두 번째 값은 number여야 함
[인덱스 시그니처]
Array는 인덱스 시그니처가 숫자로 정의되어, 모든 요소에 동일한 타입을 적용합니다.
interface NumberArray {
[index: number]: number;
}
const nums: NumberArray = [1, 2, 3];
Tuple은 인덱스별로 타입을 지정할 수 있습니다.
type PersonTuple = [string, number, boolean];
const person: PersonTuple = ["Alice", 30, true];
TypeScript에는 중요한 네 가지 특별한 타입이 있습니다:
any 타입은 말 그대로 “모든 걸 허용한다”는 뜻입니다.
타입 검사를 건너뛰기 때문에, 런타임 오류를 숨기는 원인이 되기도 하죠.
let value: any = 42;
value = "hello";
value = { a: 1 };
// 타입 검사를 안 하므로, 아래도 에러 없음
value.nonExistentMethod(); // 런타임에서 ❌ 터짐
기존의 Javascript 처럼 하나의 변수에 여러 타입 데이터가 할당되었다 해제되었다 할 때 이용하게 되는데, 그럴거면 Typescript를 사용하는 의미가 없기 때문에 최대한 사용하지 않는 것이 좋습니다.
unknown은 “무슨 타입인지 아직 모른다”를 의미합니다.
하지만 any와 달리, 사용하려면 먼저 검사(좁히기)가 필요합니다.
이게 무슨 소리라면, unknown은 기본적으로 값을 저장하기만 하는 타입입니다. 이때의 값은 any와 마찬가지로 어떤 타입이든 상관없지만, 다른 변수의 값으로 저장될 수 없습니다. 그렇기 때문에 unknown을 대상으로 연산을 진행하거나 메서드를 사용하는 것도 불가능 하죠.
하지만, 아래와 같이 조건식을 통해 특정 타입이라고 보장하게 된다면 그때부터 타입이 보장된 타입으로 변경되게 됩니다. 이러한 과정을 Type Narrowing(타입 좁히기)라고 하죠.
let input: unknown;
input = 5;
input = "text";
// 그냥 쓰면 에러
// console.log(input.toUpperCase()); // ❌
// 타입 좁히기 후엔 OK
if (typeof input === "string") {
console.log(input.toUpperCase()); // ✅
}
JavaScript의 모든 타입은 any 입니다. 따라서 TypeScript의 모든 타입은 any 안에 속한다고 볼 수 있겠죠. 그렇다면 반대로 아무것도 속하지 않는 타입이 있다면 어떻게 표현해야 할까요?
TypeScript에서는 이러한 유형의 타입을 never라고 하며, 집합에서의 공집합 개념을 가집니다. 그렇기 때문에 never는 어떠한 값도 가질 수 없는 타입입니다. 보통은 아래의 예시처럼 함수가 사용한다.
주로 예외 던지기 함수나 switch문에서 빠짐없이 처리했는지 검사또는 리턴하지 않거나 무한 루프에 빠지는 경우 사용합니다.
function fail(msg: string): never {
throw new Error(msg);
}
function exhaustive(value: "A" | "B") {
switch (value) {
case "A":
return 1;
case "B":
return 2;
default:
// value는 절대 올 수 없음 → 타입은 never
const _check: never = value; // 새로운 케이스 추가 시 에러
return _check;
}
}
[never에 대한 더 깊은 이야기]
사실 Typescript에서 never은 하나의 글로써 다루어야 할 만큼 중요한 타입입니다. 관련 내용이 더 궁금하신 분들은 이 글을 추천드릴게요.
void는 함수가 아무 값도 반환하지 않음을 나타냅니다.
보통 콘솔 출력, 로그 기록처럼 결과가 필요 없는 함수에 붙습니다.
function log(message: string): void {
console.log(message);
}
let result: void = log("hi"); // result는 undefined
TypeScript의 타입 시스템은 집합론의 개념을 많이 차용합니다.
그중 대표적인 것이 바로 Union(합집합)과 Intersection(교집합)입니다.
|) — 여러 타입 중 하나Union은 “A이거나 B”를 표현합니다.
즉, 여러 타입 중 하나를 허용하는 것입니다.
type Id = number | string;
let userId: Id;
userId = 101; // OK
userId = "admin"; // OK
// userId = true; // ❌ Error: 'boolean'은 허용되지 않음
실무에서는 함수 매개변수에 다양한 입력을 받도록 할 때 자주 사용합니다.
function toNumber(id: number | string): number {
if (typeof id === "string") {
return Number(id);
}
return id; // number
}
&) — 여러 타입을 모두 만족Intersection은 “A이면서 동시에 B”를 표현합니다.
즉, 모든 타입을 합쳐서 하나의 타입으로 만듭니다.
type A = ({ a: 1 } | { b: 2 | 3 }) & ({ b: 3 | 4 } | { c: 4 });
위 예제는 Union Type끼리 Intersection 을 진행하는 예제인데요.
해당 타입의 결과를 보면 다음과 같습니다.

무슨 말이냐면 타입에서 말하는 Intersection(하나의 타입을 만든다)라는 뜻은, Intersection에 참여하는 모든 타입에 허용되는 가장 적절한 타입을 만드는 것이라는 겁니다. 때문에 위 예제의 결과는 &(intersection)의 좌항과 우항으로 만들 수 있는 모든 Type 조합의 Union이 되는 것이죠.
Union 타입에서의 Intersection은 두 Union의 공통된 부분만을 뽑아서 새로운 타입을 만드는데 사용이 되었습니다. 하지만, Interface 타입 간의 Intersection은 다릅니다.
interface Person {
name: string;
age: number;
}
interface Address {
address: string;
}
type PersonWithAddress = Person & Address;
위 예제에서 PersonWithAddress는 Union 타입 간의 Intersection을 생각해보면 공집합을 뜻하는 never이 되어야합니다. 하지만, 결과는 Person과 Address의 합집합인 { name: string; age: number; address: string } 의 형태를 가지게 되죠.
이렇게 되는 이유는 위에서 말한 것 처럼 Typescript의 Intersection이 단순히 교집합을 나타내는 것이 아니라 “두 타입이 허용되는 가장 적절한 타입”을 반환하는 것이기 때문입니다. 때문에 Union 타입 끼리의 intersection은 각 타입의 특징을 다 표현할 수 있는 타입이 선택되는 것이며, Interface 타입 끼리의 intersection은 각 Interface의 공통된 특징만이 남은 형태가 선택되는 것이죠.
[타입 추론]
사실 union과 intersection에 대해서 제대로 이해하기 위해서는 Typescript에서의 타입 추론(Type Inference) 과정을 알아야합니다. 여기에는 구조적 타이핑, 덕 타이핑 등의 개념들이 있는데 관심 있으신 분들은 한 번 찾아보시길 추천드립니다.
이번 글에서는 Typescript를 사용하기 위해서 기본이 되는 Type 관련 개념을 정리해보았습니다. 재미있는 이야기는 아니었을지도 모르겠지만, Type이라는 개념은 모든 프로그래밍 언어에서 사용되는 필수 부가결한 요소이며 Typescript에서는 그 요소의 중요도와 활용도를 최대한으로 늘려두었다고 생각합니다. 이번을 통해 Typescript에 조금이라도 더 관심이 생긴 분들이 있으면 좋겠네요.