왜 톺아보는가?
따로 TypeScript를 공부하지는 않고, 개발을 하면서 모르는 부분을 검색하며 학습을 했습니다. 개발을 할 때는 필요하면 type을 바로 추가하거나, 오류가 나는 부분은 적당하게 맞춰서 type을 작성하다보니, 크게 어려움을 느끼지 못 했습니다.
최근에 오픈소스를 기여하게 되면서, 어능정도 정형화된 컨벤션과 type을 관리하는 방법들을 잘 소화하지 못 해 삽질 하게 되면서 한번 톺아봐야겠다 라고 결론을 내렸습니다.
TypeScript
왜 사용하는 걸까?
프로젝트를 만들어서 개발을 하게되면, JavsScript 또는 TypeScript로 언어를 선택하게 됩니다. 보통 TypeScript는 타입을 지정해줘야하는 번거로움 때문에 JavaScript를 사용하는 사람도 많습니다.
'TypeScript' 는 JavaScript 기반의 정적 타입 문법을 추가한 프로그래밍 언어입니다.
'JavaScript' 는 대표적인 동 적 타입 언어입니다. 배우기 쉽고 유연한 장점이 있지만, 동시에 런타입 에러 발생 가능성을 가지고 있습니다. 여러 의존성이 겹친 규모가 큰 프로젝트에서는 에러가 어디서 발생하는지 알기가 쉽지 않습니다.
하지만, TypeScript는 정적 타입 언어로 변수의 타입을 명시적으로 지정해서 사용합니다.
컴파일 단계에서 타입 체크를 하여 코드 작성 중 타입 오류를 방지하고, 타입 체킹 기능인 '타입 어노테이션' 을 활용하여 에러를 빠르게 처리할 수 있습니다.
어떻게 설정할까?
TypeScript를 사용하기 위해서는 tsconfig.json이 필요합니다.
컴파일 옵션을 설정 할 수 있으며, 여러가지 오류의 범위를 지정할 수 있습니다.
다음 동작원리를 보면서 왜 이 과정이 필요한지 알아봅시다.
출처: 한 입 크기로 잘라먹는 타입스크립트 | 더 자세한 내용
동작원리
JavaScript의 컴파일 과정을을 보면 코드가 AST로 변환하고 AST를 다시 바이트 코드로 변환되어서 이렇게 변환된 바이트코드를 컴퓨터가 실행하게 됩니다.
이게 대다수의 프로그래밍 언어의 동작 방식입니다.
그러면 TypeScript는 어떻게 동작할까요?
마찬가지로 TypeScript도 AST로 변환합니다. 그리고 AST에서 Type Checking을 하게 됩니다.
성공 시 JavaScript의 컴파일 과정으로 변환이 됩니다. 실패 시 컴파일이 종료되구요.
따라서 TypeScript 컴파일 시 미리 타입 체킹 이 되니 보다 안전하게 컴파일이 가능합니다.
TypeScript 흔히 잘 모르는 것들
타입 계층도
위 계층도를 알고 계시면, 타입을 상속하거나 다른 타입에 할당 가능한 지 알 수 있습니다.
슈퍼타입(상위 타입) 은 더 넓은 범위의 타입이고, 서브 타입(하위 타입) 은 더 구체적인 타입입니다.
업 캐스팅 : 서브 타입의 값을 슈퍼타입의 값으로 취급하는 것 ( 대부분 가능 ) 다운 캐스팅 : 슈퍼 타입의 값을 서브 타입의 값으로 취급하는 것 ( 대부분 불가능 )
타입 단언과 타입 좁히기 그리고 유니온 타입
여러 복잡한 타입 속에서 타입 추론을 하다보면 내부 코드가 타입 추론 을 하지 못하고 빨간색 에러를 나타나게 되는데 이 때 AI에게 도움을 받으면 옵셔널 체이닝(?.)과 as any로 그냥 에러를 급하게 막게 됩니다.
협업 시 타입 관련 오류를 찾지 못 하거나, 이상한 값을 불러와서 오히려 생산성을 저하 시킬 수 있습니다.
그래서 타입 단언, 타입 좁히기 그리고 유니온 타입 을 적절히 사용하여 타입을 관리하면 클린 코드 측면에서 효율적으로 코드를 작성할 수 있습니다.
1// 타입 단언 (as 키워드)2const element = document.getElementById('myId') as HTMLInputElement;3
4// 타입 좁히기5function processValue(value: string | number) {6 if (typeof value === 'string') {7 // 이 블록에서는 value가 string 타입으로 좁혀짐8 return value.toUpperCase();9 }10 // 이 블록에서는 value가 number 타입11 return value.toFixed(2);12}13
14// 유니온 타입과 함수 오버로딩을 통한 타입 좁히기15function getData(id: string): string;16function getData(id: number): number;17function getData(id: string | number): string | number {18 if (typeof id === 'string') {19 return `Data for ${id}`;20 }21 return id * 2;22}23
24// 사용자 정의 타입 가드25function isString(value: unknown): value is string {26 return typeof value === 'string';27}Q. 제네릭 타입 변수를 사용하면 보다 깔끔하게 코드 작성이 가능하지 않나요? A. 제네릭은 미리 정해진 타입 구조에서 타입만 바뀌는 경우 적절하고, 타입 좁히기는 런타입에 실제 값을 보고 타입을 판단해야 하는 경우가 적절하여, 둘 다 상황에 맞게 사용해야 합니다.
인터페이스와 타입
1// 인터페이스 - 확장 가능2interface User {3 name: string;4 age: number;5}6
7interface User {8 email: string; // ✅ 선언 병합 가능9}10
11interface Admin extends User {12 role: string;13}14
15// 타입 별칭 - 더 유연함16type UserType = {17 name: string;18 age: number;19}20
21// type UserType = { email: string } // ❌ 중복 선언 불가22
23type AdminType = UserType & {24 role: string;25}26
27// 유니온 타입, 원시값 등은 type만 가능28type Status = 'loading' | 'success' | 'error';29type ID = string | number;둘 다 타입을 정의하지만, 미묘한 차이가 있습니다.
확장 가능성이 있는 객체 구조 : interface 유니온, 교차 타입, 복합 타입: type
프로젝트 마다 타입을 어떻게 관리 할 지 다르다고 합니다. 저는 interface로 관리하고, 유니온 타입이 필요하다면 type으로 선언하는 편 입니다 !
제너릭? 제너릭이 핵심 문법
제네릭이란?
제네릭은 함수나 인터페이스, 타입 별칭, 클래스 등을 다양한 타입과 함께 동자갛도록 만들어 주는 타입 스크립트의 핵심 기능 중 하나 입니다.
제네릭이 필요한 상황
1// 제네릭 없이 만든다면...2function getFirstString(arr: string[]): string {3 return arr[0];4}5
6function getFirstNumber(arr: number[]): number {7 return arr[0];8}9
10function getFirstBoolean(arr: boolean[]): boolean {11 return arr[0];12}13
14// 이런 식으로 무한히 만들어야 함...해당 타입의 맞는 메서드를 사용할 때, 위에서 배운 것 처럼 타입 좁히기 를 사용하면 타입 추론이 되어서 사용가능합니다, 그런데 이 문제를 더 간단하게 해결 가능합니다.
1function getFirst<T>(arr: T[]): T {2 return arr[0];3}4
5// 이제 모든 타입에서 사용 가능6const firstString = getFirst(['a', 'b', 'c']); // string7const firstNumber = getFirst([1, 2, 3]); // number8const firstUser = getFirst([{name: 'John'}, {name: 'Jane'}]); // {name: string}이렇듯 인수에 따라서 가변적인 타입을 정할 수 습니다.
다양한 예제를 보면
1// 기본 제너릭2function identity<T>(arg: T): T {3 return arg;4}5
6const stringResult = identity<string>("hello"); // string7const numberResult = identity(42); // 타입 추론으로 number8
9// 제약 조건이 있는 제너릭10interface Lengthwise {11 length: number;12}13
14function logLength<T extends Lengthwise>(arg: T): T {15 console.log(arg.length);16 return arg;17}18
19// 여러 제너릭 매개변수20function merge<T, U>(obj1: T, obj2: U): T & U {21 return { ...obj1, ...obj2 };22}23
24// 조건부 타입과 함께25type NonNullable<T> = T extends null | undefined ? never : T;-
제약 조건 (Constraints) 때로는 제네릭 타입이 특정 조건을 만족해야 할 때가 있습니다. extends 키워드를 사용해서 제약을 걸 수 있습니다.
-
여러 제네릭 매개변수 여러 개의 제네릭 타입을 사용할 수도 있습니다.
-
조건부 타입과 함께 제네릭과 조건부 타입을 함께 사용하면 더 복잡한 타입 로직을 만들 수 있습니다.
인덱스드 엑세스 타입, keyof,맵드, 템플릿 리터럴 타입
TypeScript의 고급 타입 기능들입니다. 처음에는 복잡해 보이지만, 이해하고 나면 정말 강력한 도구들입니다.
keyof 연산자 - 객체 타입의 모든 키를 유니온 타입으로 만들어주는 연산자
1type Person = {2 name: string;3 age: number;4 address: string;5}6
7type PersonKeys = keyof Person; // "name" | "age" | "address"객체의 키만 받는 받는 함수를 만들때 안전하게 사용 가능합니다.
인덱스드 엑세스 타입 - 이미 정의된 타입에서 특정 속성의 타입만 뽑아낼 때 사용합니다.
1type Person = {2 name: string;3 age: number;4 address: {5 city: string;6 country: string;7 }8}9
10type PersonName = Person["name"]; // string11type PersonAddress = Person["address"]; // { city: string; country: string; }12type PersonCity = Person["address"]["city"]; // string13
14// 배열의 요소 타입도 가져올 수 있음15type Users = User[];16type SingleUser = Users[number]; // User맵드 타입 - 기존 타입을 '변환'해서 새로운 타입을 만드는 방법입니다. 객체의 모든 속성에 동일한 변환을 적용할 때 사용
1type Person = {2 name: string;3 age: number;4 email: string;5}6
7// 모든 속성을 선택적으로 만들기8type OptionalPerson = {9 [K in keyof Person]?: Person[K];10}11// 결과: { name?: string; age?: number; email?: string; }12
13// 모든 속성을 읽기 전용으로 만들기14type ReadonlyPerson = {15 readonly [K in keyof Person]: Person[K];16}17// 결과: { readonly name: string; readonly age: number; readonly email: string; }템플릿 리터럴 타입 - 타입 레벨에서 문자열을 조합할 수 있는 기능입니다.
1type Greeting = "hello" | "hi";2type Name = "world" | "typescript";3
4type GreetingMessage = `${Greeting} ${Name}`;5// "hello world" | "hello typescript" | "hi world" | "hi typescript"유틸리티 타입들
TypeScript에서 기본으로 제공하는 유틸리티 타입들은 정말 자주 사용하게 됩니다. 다양한 예시를 보여드리겠습니다 !
Partial<T> - 모든 속성을 선택적(optionoal)으로 만들어주는 타입입니다.
1interface User {2id: number;3name: string;4email: string;5age: number;6}7
8type PartialUser = Partial<User>;9// { id?: number; name?: string; email?: string; age?: number; }10
11// 실무 예시: 사용자 정보 업데이트12function updateUser(id: number, updates: Partial<User>) {13// id는 필수, 나머지는 선택적으로 업데이트14}15
16updateUser(1, { name: "새 이름" }); // ✅17updateUser(1, { email: "new@email.com", age: 25 }); // ✅