[With AI] Typescript A 부터 Z 까지
TIP
본 게시물은 AI(Claude)와 함께 작성하였습니다.
참고자료
이펙티브 타입스크립트: 동작 원리의 이해와 구체적인 조언 62가지
타입스크립트에서 타입 프로그래밍하기: SmartEditor에서 타입을 다루는 방법 - Naver SmartStudio Blog
type-challenges/README.ko.md at main · type-challenges/type-challenges
[10분 테코톡] 시지프의 타입스크립트 도약하기
1. 타입스크립트 소개
What (타입스크립트란 무엇인가?)
TypeScript는 Microsoft에서 개발한 오픈 소스 프로그래밍 언어로, JavaScript의 슈퍼셋입니다. 즉, 모든 JavaScript 코드는 유효한 TypeScript 코드입니다. TypeScript는 다음과 같은 특징을 가집니다:
- 정적 타입 지원
- 클래스 기반 객체 지향 프로그래밍
- 모듈 시스템
- ES6+ 기능 지원
// TypeScript 예시
let greeting: string = "Hello, TypeScript!";
console.log(greeting);
Why (왜 타입스크립트를 사용해야 하는가?)
- 향상된 개발자 경험:
- 코드 자동 완성 및 IntelliSense 지원
- 리팩토링 시 더 안전하고 효율적인 작업 가능
- 오류 조기 발견:
- 컴파일 시점에 타입 관련 오류 감지
- 런타임 오류 감소
- 더 나은 코드 문서화:
- 타입 정의를 통한 자체 문서화
- 코드 가독성 및 유지보수성 향상
- 대규모 애플리케이션 개발에 적합:
- 복잡한 시스템 설계에 유용한 타입 시스템
- 팀 협업 시 코드 품질 유지 용이
Don't (언제 타입스크립트를 사용하지 말아야 하는가?)
- 소규모 또는 간단한 프로젝트:
- 설정 및 컴파일 과정이 오버헤드가 될 수 있음
- 빠른 프로토타이핑이 필요한 경우
- 팀이 TypeScript에 익숙하지 않은 경우:
- 학습 곡선으로 인한 초기 생산성 저하 가능성
- 런타임 성능이 극도로 중요한 경우:
- 컴파일된 JavaScript가 약간의 오버헤드를 가질 수 있음
- 단, 대부분의 경우 성능 차이는 무시할 만한 수준
- 특정 JavaScript 라이브러리와의 호환성 문제:
- 타입 정의 파일이 없거나 불완전한 경우 사용이 어려울 수 있음
2. 기본기
기본 타입
// 기본 타입 예제
let id: number = 5;
let company: string = "Acme Corp";
let isPublished: boolean = true;
let x: any = "Hello";
// 배열
let ids: number[] = [1, 2, 3, 4, 5];
let arr: any[] = [1, true, "Hello"];
// 튜플
let person: [number, string, boolean] = [1, "Brad", true];
// 열거형
enum Direction {
Up = 1,
Down,
Left,
Right,
}
인터페이스와 타입 별칭
// 인터페이스
interface UserInterface {
readonly id: number;
name: string;
age?: number;
}
const user1: UserInterface = {
id: 1,
name: "John",
};
// 타입 별칭
type Point = {
x: number;
y: number;
};
const pt: Point = { x: 10, y: 20 };
함수와 타입
// 함수
function addNum(x: number, y: number): number {
return x + y;
}
// 선택적 매개변수
function log(message: string, userId?: string) {
console.log(message, userId || "Not signed in");
}
// 함수 오버로딩
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
function padding(a: number, b?: number, c?: number, d?: number) {
if (b === undefined && c === undefined && d === undefined) {
b = c = d = a;
} else if (c === undefined && d === undefined) {
c = a;
d = b;
}
return {
top: a,
right: b,
bottom: c,
left: d
};
}
제네릭
// 제네릭 함수
function getArray<T>(items : T[]) : T[] {
return new Array().concat(items);
}
let numArray = getArray<number>([1, 2, 3, 4]);
let strArray = getArray<string>(["a", "b", "c", "d"]);
// 제네릭 인터페이스
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
타입 추론과 타입 단언
// 타입 추론
let x = 3; // TypeScript가 x를 number로 추론
// 타입 단언
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
3. 타입 유틸리티
TypeScript는 복잡한 타입을 쉽게 조작할 수 있는 여러 유틸리티 타입을 제공합니다. 이들은 실제 개발 상황에서 매우 유용하게 사용됩니다. 주요 유틸리티 타입과 그 사용 예를 살펴보겠습니다.
Partial<T>
모든 속성을 선택적으로 만듭니다.
AS-IS:
interface User {
id: number;
name: string;
email: string;
}
function updateUser(user: User) {
// 모든 필드를 업데이트해야 함
}
TO-BE:
interface User {
id: number;
name: string;
email: string;
}
function updateUser(user: Partial<User>) {
// 일부 필드만 업데이트 가능
}
updateUser({ name: "John" }); // 유효
Required<T>
모든 속성을 필수로 만듭니다.
AS-IS:
interface Config {
debug?: boolean;
timeout?: number;
}
TO-BE:
type RequiredConfig = Required<Config>;
// 결과: { debug: boolean; timeout: number; }
const config: RequiredConfig = {
debug: true,
timeout: 3000
}; // 모든 필드 필수
Pick<T, K>
특정 속성만 선택합니다.
AS-IS:
interface Product {
id: number;
name: string;
price: number;
description: string;
}
TO-BE:
type ProductPreview = Pick<Product, 'name' | 'price'>;
// 결과: { name: string; price: number; }
const preview: ProductPreview = {
name: "Laptop",
price: 1000
};
Omit<T, K>
특정 속성을 제외합니다.
AS-IS:
interface User {
id: number;
username: string;
password: string;
}
TO-BE:
type PublicUser = Omit<User, 'password'>;
// 결과: { id: number; username: string; }
const publicInfo: PublicUser = {
id: 1,
username: "john_doe"
};
Record<K, T>
키-값 쌍의 타입을 정의합니다.
AS-IS:
const fruitInventory = {
apple: 10,
banana: 20,
orange: 15
};
TO-BE:
type Fruit = 'apple' | 'banana' | 'orange';
type Stock = Record<Fruit, number>;
const fruitInventory: Stock = {
apple: 10,
banana: 20,
orange: 15
};
Readonly<T>
모든 속성을 읽기 전용으로 만듭니다.
AS-IS:
interface Config {
apiKey: string;
timeout: number;
}
TO-BE:
type ReadonlyConfig = Readonly<Config>;
const config: ReadonlyConfig = {
apiKey: "my-secret-key",
timeout: 3000
};
// config.apiKey = "new-key"; // 오류: 읽기 전용 속성
ReturnType<T>
함수의 반환 타입을 추출합니다.
AS-IS:
function fetchUser() {
return { id: 1, name: "John", age: 30 };
}
TO-BE:
type User = ReturnType<typeof fetchUser>;
// 결과: { id: number; name: string; age: number; }
const user: User = {
id: 2,
name: "Jane",
age: 28
};
Parameters<T>
함수의 매개변수 타입을 튜플로 추출합니다.
AS-IS:
function greet(name: string, age: number) {
return `Hello, ${name}! You are ${age} years old.`;
}
TO-BE:
type GreetParams = Parameters<typeof greet>;
// 결과: [string, number]
const params: GreetParams = ["Alice", 25];
console.log(greet(...params));
실제 사용 예제: 복합 유틸리티 타입
여러 유틸리티 타입을 조합하여 복잡한 타입을 만들 수 있습니다:
interface User {
id: number;
name: string;
email: string;
password: string;
isAdmin: boolean;
}
// 관리자용 사용자 정보 타입 (비밀번호 제외, 모든 필드 읽기 전용)
type AdminSafeUser = Readonly<Omit<User, 'password'>>;
// 사용자 생성 시 필요한 정보 타입 (ID 제외, 관리자 여부 선택적)
type CreateUserDto = Omit<User, 'id'> & Partial<Pick<User, 'isAdmin'>>;
function createUser(userData: CreateUserDto) {
// 사용자 생성 로직
}
createUser({
name: "John Doe",
email: "john@example.com",
password: "securepassword123"
// isAdmin은 선택적
});
이러한 타입 유틸리티들을 활용하면 코드의 타입 안정성을 높이고, 재사용 가능한 타입을 쉽게 정의할 수 있습니다. 실제 프로젝트에서 이들을 적절히 조합하여 사용하면 더 견고하고 유지보수가 쉬운 코드를 작성할 수 있습니다.
4. 타입 추론 심화
TypeScript의 타입 추론 기능을 잘 활용하면 명시적인 타입 선언을 줄이면서도 타입 안정성을 유지할 수 있습니다. 다음은 타입 추론을 적극적으로 활용하는 예제들입니다.
변수 초기화를 통한 타입 추론
// 명시적 타입 선언 없이도 TypeScript가 타입을 추론합니다.
let name = "Alice"; // string으로 추론
let age = 30; // number로 추론
let isStudent = false; // boolean으로 추론
// 배열의 경우 요소 타입을 추론합니다.
let numbers = [1, 2, 3, 4, 5]; // number[]로 추론
let mixed = [1, "two", 3, "four"]; // (string | number)[]로 추론
함수 반환 타입 추론
function add(a: number, b: number) {
return a + b; // 반환 타입이 number로 추론됨
}
function greet(name: string) {
return `Hello, ${name}!`; // 반환 타입이 string으로 추론됨
}
객체 리터럴과 타입 추론
// 객체 리터럴의 속성 타입이 자동으로 추론됩니다.
const user = {
id: 1,
name: "John Doe",
age: 30,
isAdmin: false,
roles: ["user", "editor"]
};
// user의 타입은 다음과 같이 추론됩니다:
// {
// id: number;
// name: string;
// age: number;
// isAdmin: boolean;
// roles: string[];
// }
제네릭과 타입 추론
function identity<T>(arg: T): T {
return arg;
}
let output = identity("myString"); // output의 타입이 string으로 추론됨
let numOutput = identity(42); // numOutput의 타입이 number로 추론됨
콜백 함수의 매개변수 타입 추론
const numbers = [1, 2, 3, 4, 5];
// item의 타입이 number로 추론됨
numbers.forEach((item) => {
console.log(item.toFixed(2));
});
// 명시적 타입 선언 없이도 value의 타입이 number로 추론됨
const doubled = numbers.map(value => value * 2);
구조 분해 할당과 타입 추론
const point = { x: 10, y: 20 };
// destructured의 타입이 { x: number, y: number }로 추론됨
function printCoord({ x, y }: { x: number; y: number }) {
console.log(`Coordinate: (${x}, ${y})`);
}
printCoord(point);
조건부 타입과 타입 추론
type IsArray<T> = T extends any[] ? true : false;
// 타입 인수를 명시적으로 지정하지 않아도 TypeScript가 추론
const isNumberArray: IsArray<number[]> = true; // true로 추론
const isStringArray: IsArray<string> = false; // false로 추론
복합 예제: 타입 추론을 활용한 유틸리티 함수
function createUser<T extends object>(defaults: T, override: Partial<T>) {
return { ...defaults, ...override };
}
const defaultUser = {
name: "Guest",
age: 0,
isAdmin: false
};
// user의 타입이 { name: string; age: number; isAdmin: boolean; }로 추론됨
const user = createUser(defaultUser, { name: "John", age: 30 });
console.log(user); // { name: "John", age: 30, isAdmin: false }
5. 제네릭 심화
제네릭을 사용하면 타입을 마치 함수의 매개변수처럼 사용할 수 있어, 재사용 가능하고 유연한 코드를 작성할 수 있습니다. 다음은 제네릭을 적극적으로 활용하는 다양한 예제들입니다.
기본 제네릭 함수
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString"); // 타입을 명시적으로 지정
let output2 = identity(42); // 타입 추론 사용
제네릭 인터페이스
interface GenericIdentityFn<T> {
(arg: T): T;
}
let myIdentity: GenericIdentityFn<number> = identity;
제네릭 클래스
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, addFn: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = addFn;
}
}
let stringNumeric = new GenericNumber<string>("", (x, y) => x + y);
console.log(stringNumeric.add(stringNumeric.zeroValue, "test")); // "test"
let numberNumeric = new GenericNumber<number>(0, (x, y) => x + y);
console.log(numberNumeric.add(5, 10)); // 15
제네릭 제약조건
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 이제 .length 속성이 있다고 확신할 수 있습니다
return arg;
}
loggingIdentity([1, 2, 3]); // 성공
loggingIdentity({length: 10, value: 3}); // 성공
// loggingIdentity(3); // 오류, number에는 .length가 없습니다
제네릭과 타입 추론을 활용한 Factory 함수
function create<T>(Factory: { new(): T }): T {
return new Factory();
}
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
let bee = create(Bee);
let lion = create(Lion);
제네릭 타입 매핑
type Nullable<T> = { [P in keyof T]: T[P] | null };
interface User {
name: string;
age: number;
}
type NullableUser = Nullable<User>;
// 결과: { name: string | null; age: number | null; }
조건부 타입과 제네릭
type NonNullable<T> = T extends null | undefined ? never : T;
type Result = NonNullable<string | number | null | undefined>;
// 결과: string | number
복합 예제: 제네릭을 활용한 상태 관리 시스템
class StateManager<S> {
private state: S;
constructor(initialState: S) {
this.state = initialState;
}
getState(): S {
return this.state;
}
setState(newState: Partial<S>): void {
this.state = { ...this.state, ...newState };
}
}
interface UserState {
name: string;
age: number;
isLoggedIn: boolean;
}
const userStateManager = new StateManager<UserState>({
name: "Guest",
age: 0,
isLoggedIn: false
});
console.log(userStateManager.getState()); // { name: "Guest", age: 0, isLoggedIn: false }
userStateManager.setState({ name: "John", age: 30 });
console.log(userStateManager.getState()); // { name: "John", age: 30, isLoggedIn: false }
userStateManager.setState({ isLoggedIn: true });
console.log(userStateManager.getState()); // { name: "John", age: 30, isLoggedIn: true }
6. React와 TypeScript
React와 TypeScript를 함께 사용할 때 특히 유용한 타입 유틸리티들을 소개합니다. 이들은 컴포넌트와 props를 더 정확하고 유연하게 타입화하는 데 도움을 줍니다.
ComponentProps<T>
컴포넌트의 props 타입을 추출합니다.
AS-IS:
import { Button } from 'some-ui-library';
type ButtonProps = {
onClick: () => void;
children: React.ReactNode;
// 다른 모든 props를 수동으로 정의해야 함
};
TO-BE:
import { Button } from 'some-ui-library';
import { ComponentProps } from 'react';
type ButtonProps = ComponentProps<typeof Button>;
PropsWithChildren<P>
props 타입에 children을 추가합니다.
AS-IS:
type CardProps = {
title: string;
children?: React.ReactNode;
};
TO-BE:
import { PropsWithChildren } from 'react';
type CardProps = PropsWithChildren<{
title: string;
}>;
CSSProperties
인라인 스타일 객체의 타입을 정의합니다.
AS-IS:
function StyledDiv({ style }: { style: any }) {
return <div style={style} />;
}
TO-BE:
import { CSSProperties } from 'react';
function StyledDiv({ style }: { style: CSSProperties }) {
return <div style={style} />;
}
7. 종합 예제 5가지
재귀적 타입을 이용한 JSON 파서
이 예제는 TypeScript의 재귀적 타입을 사용하여 JSON 구조를 타입으로 표현합니다.
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
type JSONObject = { [key: string]: JSONValue };
function parseJSON(json: string): JSONValue {
return JSON.parse(json);
}
function stringifyJSON(value: JSONValue): string {
return JSON.stringify(value);
}
// 사용 예
const jsonString = '{"name":"John","age":30,"isStudent":false,"hobbies":["reading","coding"]}';
const parsed = parseJSON(jsonString);
console.log(parsed);
const stringified = stringifyJSON({ x: 5, y: [1, 2, 3] });
console.log(stringified);
조건부 타입을 이용한 유틸리티 함수
이 예제는 조건부 타입을 사용하여 객체의 특정 속성만 선택하는 유틸리티 함수를 구현합니다.
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P]
};
function pickByType<T extends object, U>(obj: T, type: new (...args: any[]) => U): PickByType<T, U> {
const result: Partial<PickByType<T, U>> = {};
for (const key in obj) {
if (obj[key] instanceof type) {
result[key as keyof PickByType<T, U>] = obj[key] as U;
}
}
return result as PickByType<T, U>;
}
// 사용 예
const mixedObject = {
name: "John",
age: 30,
birthDate: new Date("1990-01-01"),
address: { street: "Main St", city: "New York" }
};
const dateProperties = pickByType(mixedObject, Date);
console.log(dateProperties); // { birthDate: Date }
제네릭과 매핑된 타입을 이용한 깊은 읽기 전용 변환기
이 예제는 제네릭과 매핑된 타입을 사용하여 객체를 깊은 수준에서 읽기 전용으로 변환합니다.
type DeepReadonly<T> =
T extends (infer R)[] ? DeepReadonlyArray<R> :
T extends Function ? T :
T extends object ? DeepReadonlyObject<T> :
T;
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
function makeDeepReadonly<T>(obj: T): DeepReadonly<T> {
return obj as DeepReadonly<T>;
}
// 사용 예
const mutableObject = {
name: "John",
age: 30,
address: {
street: "Main St",
city: "New York"
},
hobbies: ["reading", "coding"]
};
const readonlyObject = makeDeepReadonly(mutableObject);
// readonlyObject.name = "Jane"; // 오류
// readonlyObject.address.city = "Boston"; // 오류
// readonlyObject.hobbies.push("gaming"); // 오류
인터섹션 타입과 유니온 타입을 이용한 복잡한 상태 관리
이 예제는 인터섹션 타입과 유니온 타입을 사용하여 복잡한 애플리케이션 상태를 모델링합니다.
type UserRole = "admin" | "user" | "guest";
interface BaseState {
isLoading: boolean;
error: string | null;
}
interface GuestState extends BaseState {
userRole: "guest";
}
interface UserState extends BaseState {
userRole: "user";
username: string;
}
interface AdminState extends BaseState {
userRole: "admin";
username: string;
adminToken: string;
}
type AppState = GuestState | UserState | AdminState;
function updateState(currentState: AppState, update: Partial<AppState>): AppState {
return { ...currentState, ...update } as AppState;
}
// 사용 예
let state: AppState = { isLoading: true, error: null, userRole: "guest" };
console.log(state);
state = updateState(state, { isLoading: false, userRole: "user", username: "john_doe" });
console.log(state);
state = updateState(state, { userRole: "admin", adminToken: "secret_token" });
console.log(state);
타입 추론과 제네릭을 이용한 함수 합성기
이 예제는 타입 추론과 제네릭을 사용하여 함수 합성을 타입 안전하게 구현합니다.
type Func<T extends any[], R> = (...args: T) => R;
function compose<R>(fn1: Func<any, R>): Func<any, R>;
function compose<T1, R>(fn1: Func<[T1], R>): Func<[T1], R>;
function compose<T1, T2, R>(fn1: Func<[T2], R>, fn2: Func<[T1], T2>): Func<[T1], R>;
function compose<T1, T2, T3, R>(
fn1: Func<[T3], R>,
fn2: Func<[T2], T3>,
fn3: Func<[T1], T2>
): Func<[T1], R>;
function compose(...fns: Func<any, any>[]): Func<any, any> {
return fns.reduce((f, g) => (...args) => f(g(...args)));
}
// 사용 예
const toUpperCase = (x: string) => x.toUpperCase();
const exclaim = (x: string) => x + "!";
const shout = compose(exclaim, toUpperCase);
console.log(shout("hello")); // "HELLO!"
const add = (a: number, b: number) => a + b;
const square = (x: number) => x * x;
const addAndSquare = compose(square, add);
console.log(addAndSquare(3, 4)); // 49