Typescript

[TypeScript] TS Cheat Sheet (Type, Interface)

sihanni 2025. 8. 19. 22:54

1. Type

개요

 우리가 평소에 줄여 말하는 Type의 전체 이름은 'type alias(타입 별칭)'이며 타입 리터럴에 이름을 붙이는데 사용된다.
새로운 타입을 만드는 것이 아니라, 기존 특정 타입에 새로운 이름을 부여하여 코드의 가독성을 높이고 재사용성을 향상시키는 기능이다. 즉, 이미 존재하는 타입을 마치 변수처럼 다루면서, 복잡한 타입을 간단하게 표현하거나, 여러 곳에서 동일한 타입 정의를 재사용할 수 있도록 돕는다.

 type UserName = string;
(키워드) (타입 별칭)   (타입)
type UserId = number;
type ProductId = number;

둘 다 숫자지만, 의미가 다르다. alias를 사용함으로써 코드를 읽는 사람은 이 숫자가 유저 ID인지, 상품 ID인지 바로 알 수 있다.

type ApiResponse = { code: number; data: string; error?: string };

이렇게 복잡한 구조를 alias로 묶어두면 매번 풀어서 쓰지않아도 되고, 이름 하나로 재사용이 가능해 유지보수와 가독성이 좋아진다.

type Status = "pending" | "success" | "error";
type Coordinate = [number, number];

특히 뒤에서 알아볼 유니온 타입이나 튜플 같은 리터럴 타입은 alias로 정의해두면 훨씬 읽기가 쉬워진다.

type UserType = {
  id: number;
  name: string;
};

 

타입을 변수 처럼 생각하자.

서로 다른 스코프에서 같은 이름의 변수를 만들 수 있는 것처럼, 타입도 비슷한 의미 체계를 가진다.


기본 타입 (Primitive Type)

// 문자열 타입: string
const name: string = "Lee";

// 숫자 타입: number
const age: number = 20;

// boolean 타입
const isLogin: boolean = false;

// Array 타입
const companies: Array<string> = ['삼성', 'LG', '현대'];
const companies: string[] = ['삼성', 'LG', '현대'];

// Tuple 타입
const items: [string, number] = ['hamilton', 44];
// 튜플: 배열 길이가 고정되고 각 요소 타입이 정의된 배열
  • string (문자열)
  • number (숫자, 정수, 실수 모두 포함),
  • boolean (true/ false)
  • object
  • Array
  • tuple
  • any
  • null (값이 "없음"을 명시적으로 표현)
  • undefined (값이 아직 정의가 안되었을 때)

any, unknown, null, undefined, never

any : 모든 타입을 허용(타입 검사를 무력화하기 때문에 위험함. 실무에서는 지양하는 것이 좋다)

컴파일러가 타입 체크를 하지 않는다. any가 섞이는 순간, 그 값을 쓰는 모든 곳이 타입 안전성을 잃게 된다.

만약 외부 라이브러리에서 타입 정의가 없을 때 any 대신 unknown을 써서 타입 가드를 통해 안전하게 처리할 수 있다.

 

unknown : 모든 값을 담을 수 있지만, 사용하려면 타입 체크가 필요하다. any보다는 안전하다.

let value: unknown = "hello";

value.toUpperCase(); //  오류
if (typeof value === "string") {
  console.log(value.toUpperCase()); //  안전
}

 

null : "의도적으로 값이 없음"을 표현하는 타입이며 타입 정의에 포함시키지 않으면 들어올 수 없다.

let name: string | null = null;
if (name === null) {
  console.log("값 없음");
}

 

undefined : 변수를 선언했지만 "값을 아직 할당하지 않은 상태". 함수에서 return을 생략하면 undefined를 반환한다.

let a: number | undefined;
console.log(a); // undefined

function foo(): void {}
console.log(foo()); // undefined

 

never : 절대 존재하거나 발생할 수 없는 값. "도달할 수 없는 값". 절대 반환되지 않는 상황을 표현할 때 사용한다.

// 함수가 정상적으로 끝까지 실행되지 않고, 예외를 던져버리는 경우
function throwError(message: string): never {
  throw new Error(message);
}
// 이 함수는 어떤 값도 반환하지 않는다.
// 이 함수가 실행된다면 무조건 throw로 끝나므로 반환 타입은 never이다.
  •  never 과 void
    • never: 절대 반환 불가능하다는 의미이며 예외나 무한 루프처럼 함수가 끝나지 않음.
    • void: “값이 없거나 무시됨” → 실행은 정상적으로 끝남.

타입 단언 (as) vs Non-null 단언 (!)

const el = document.querySelector("input") as HTMLInputElement;
el.value = "Hello";

타입 단언 (Type Assertion: as) 는 "이 값은 특정 타입이다" 라고 강제로 단언하는 문법이다.

잘못 사용하면 런타임 에러 위험이 있다.

function process(user?: User) {
  console.log(user!.id); // user가 null/undefined 아님을 강제 단언
}

값이 null이나 undefined가 아님을 보장할 때 사용한다.

 

옵셔널 체이닝(?.), Null 병합 연산자(??)

옵셔널 체이닝: 값이 null/undefined일 수 있을 때 안전하게 접근

console.log(user?.profile?.email);

중간에 하나라도 null/undefined 이라면 전체가 undefined

 

Null 병합 연산자 : 좌항이 null 또는 undefined일 때만 우항을 반환한다.

const age = user.age ?? 20;  
// (user.age가 null또는 undefined라면 20을 반환한다.)

falsy(0, false, "")는 그대로 유지된다. ||와는 다르다.

 

Object Literal Type

type Location = {
  x: number;
  y: number;
};

 

Tuple Type 

튜플은 특정 인덱스에 정해진 타입을 가진 특별한 형태의 배열이다. (일반적으로 튜플은 한 번 생성하면 수정할 수 없다.)

type Data = [
  location: Location,
  timestamp: string
];

 

Union Type 

많은 옵션 중 하나를 표현하는 타입으로, 예를 들면 문자열 목록 같은 것

여러 개의 타입 중 한 개만 쓰고 싶을 때 사용하는 문법

type Size = "small" | "medium" | "large"
function logText(text: string | number) {
  if (typeof text === 'string'){
    console.log(text.toUpperCase());
  }
  if (typeof text === 'number'){
    console.log(text.toLocaleString());
  }
}

 

Discriminated Union (태그된 유니온 타입)

유니온 타입 ( | )을 정의할 때, 공통 태그 필드를 하나 두어 타입을 구분하는 방식이며, 타입 좁히기 (type narrowing)에 매우 유용하다.

type Shape = 
  | { kind: "circle"; radius: number}
  | { kind: "square"; size: number}
  | { kind: "triangle"; base: number; height: number}
  
function area(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size * shape.size;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}

console.log(area({ kind: "circle", radius: 10 })); // 314.15...

상태 관리 (loading | success | error) 같은 상태를 모델링할 때나, 이벤트 처리 중 이벤트를 구분할 때 유용하다.

 

Interserction Type

타입을 병합하거나 확장하는 방법. 보통 인터페이스 2개를 합치거나 타입 정의 여러 개를 하나로 합칠 때 사용된다. 

보통 extends 대신에 &를 쓰는 경우도 많다.

type Location =
  { x: number } & { y: number }

// { x: number, y: number}

type Person = {name: string} & {age:number};

const user:Person = {name: "Lee", age:30};

 

Type Indexing

타입의 일부를 추출해 이름을 붙이는 방법

type Response = { data: { ... } }
type Data = Response["data"]
// { ... }

 

Type from Value

typeof 연산자를 사용해 기존 자바스크립트 런타임 값에서 타입을 재사용할 수 있다.

const data = { ... }
type Data = typeof data

 

Type from Func Return

함수의 반환값을 타입으로 재사용할 수 있다.

const createFixtures = () => { ... }
type Fixtures = ReturnType<typeof createFixtures>

function test(fixture: Fixtures) {}

 

Type from Module

const data: import("./data").data

 

Mapped Types

타입 시스템에서 map 구문처럼 동작하여, 입력 타입을 새로운 타입 구조로 바꿀 수 있다.

type Artist = { name: string, bio: string };

type Subscriber<Type> = {
  [Property in keyof Type]:
    (newValue: Type[Property]) => void
}

type ArtistSub = Subscriber<Artist>
// { name: (nv: string) => void,
//   bio: (nv: string) => void }

 

Conditional Types

타입 시스템 안에서 if문 처럼 동작한다. 제네릭을 통해 만들어지며, 주로 타입 유니온의 옵션 수를 줄이는 데 사용된다.

type HasFourLegs<Animal> =
  Animal extends { legs: 4 } ? Animal : never
  
type Animals = Bird | Dog | Ant | Wolf;
type FourLegs = HasFourLegs<Animals>

// Dog | Wolf

 

Template Union Types

템플릿 문자열을 사용해 타입 시스템 안에서 텍스트를 결합하거나 조작할 수 있다.

type SupportedLangs = "en" | "pt" | "zh";
type FooterLocaleIDs = "headers" | "footer";

type AllLocaleIds = 
  `${SupportedLangs}+${FooterLocaleIDs}_id`;

// "en_header_Id" | "en_footer_id"

 


2. Interface

 

개요

 "객체 타입을 정의할 때 사용하는 문법"

객체의 형태를 설명하는 데 사용되며, 다른 인터페이스에 의해 확장될 수 있다. 자바스크립트의 거의 모든 것은 객체이고, 인터페이스는 그들의 런타임 동작에 맞추어 만들어졌다. 객체의 타입을 정의할 때 사용하고, 객체가 활용되는 모든 곳에 인터페이스를 쓸 수 있다.

 

내장된 기본 타입

boolean, string, number, undefined, null, any, unknown, never, void, bigint, symbol

 

내장 객체 및 기본 타입

Date, Error, Array, Map, Set, Regexp, Promise

 


기본 형태 : 객체 타입 정의

interface User {
  name: string;
  age: number;
  phone?: string;
}

 

Generics

인터페이스 안에서 변할 수 있는 타입을 선언한다.

interface APICall<Response> {
  data: Response
}

const api: APICall<ArtworkCall> = ...
api.data  // Artwork

또한, extends 키워드를 사용해서 제네릭 매개변수에 허용되는 타입을 제한할 수 있다.

interface APICall<Response extends { status: number }> {
  data: Response
}
const api: APICall<{ status: number }> = ...

api.data.status

 

인터페이스 상속 ( extends )

상속은 객체 간 관계를 형성하는 방법이며, 상위 클래스의 내용을 하위 클래스가 물려받아 사용하거나 확장하는 기법을 의미한다.

이런 클래스와 마찬가지로 인터페이스에서도 동일하게 extends 예약어를 통해 상속할 수 있다.

인터페이스를 상속하여 사용할 때는 부모 인터페이스에 정의된 타입을 자식 인터페이스에서 모두 보장해주어야 한다.

interface Person {
  name: string;
  age: number;
}

interface Racer extends Person {
  skill: string;
}

const Hamilton: Racer = {
  name: '해밀턴',
  age: 40,
  skill: 'dignity of respect'
}

 

Overloads

호출 가능한 인터페이스는 매개변수 집합에 따라 여러 정의를 가질 수 있다.

오버로드 시그니처를 선언하게되면, 구현부에서는 그중 하나를 모두 커버하는 실제 타입을 반환해야 한다.

interface ToArray {
  (value: string): string[];
  (value: string[]): string;
}

const toArray: ToArray = (value: any) => {
  if (typeof value === "string"){
    return value.split("");
  }
  if (Array.isArray(value)){
    return value.join("");
  }
  throw new Error("Unsupported Type")'
};

const x = toArray("ABC");     // ["A", "B", "C"]
const y = toArray(["A","B"]); // "AB"

 

Get & Set (Getter & Setter 오버로드, 읽기/쓰기 타입 분리)

TypeScript에서 객체는 커스텀 게터(읽기)타입과 세터(쓰기)타입을 서로 다르게 둘 수 있다. 

interface Ruler {
  get size(): number;                 // 읽을 때는 number
  set size(value: number | string);   // 쓸 때는 number | string 허용
}

class InchRuler implements Ruler {
  private _size = 0;

  get size(): number {
    return this._size;
  }

  set size(value: number | string) {
    const n = typeof value === "string" ? Number(value) : value;
    if (!Number.isFinite(n) || n < 0) {
      throw new Error("유효한 길이를 입력하세요");
    }
    this._size = n;
  }
}

const r: Ruler = new InchRuler();
r.size = 12;     // OK
r.size = "36";   // OK
const current: number = r.size; // 읽을 때는 항상 number

읽기에 대한 타입과 쓰기에 대한 타입을 분리해서 사용성(입력 편의)과 안정성(출력 일관성)을 동시에 잡을 수 있다.

 

인터페이스 병합 (Declaration Merging)

인터페이스는 병합이 되므로, 여러 번 선언하면 멤버가 합쳐진다. 기존에 만들어진 인터페이스에 내용을 추가하는 경우 유용하다.

interface APICall {
  data: Response
}
interface APICall {
  error?: Error
}

// 결과
interface APICall {
  data: Response;
  error?: Error;
}

병합 규칙

  • 서로 다른 멤버 이름은 그대로 추가된다.
  • 같은 이름의 프로퍼티는 타입과 선택성(옵셔널 여부)이 동일해야하고, 다르면 충돌에러가 난다.
    • (예: 한쪽은 data: Response, 다른 쪽은 data?: Response → 에러)
  • 메서드 시그니처(오버로드)는 합쳐지며, 나중에 선언된 것이 앞쪽(우선순위 높게)으로 배치된다.
  • 인덱스 시그니처는 서로 호환되야하며, 불일치할 경우 에러가 발생한다.
  • 같은 이름의 멤버이지만 종류가 다르면(예: 한쪽은 프로퍼티, 한쪽은 메서드) 에러가 발생한다.
  • 만약 기존 인터페이스에서 name: string; 으로 되어있고 name: number; 을 추가하려하면 컴파일 에러가 발생한다. 
    • 이런 경우는 원본 선언 자체를 수정하던지, 유니온으로 넓히는 것이 좋다. 또는 별도 타입으로 교체해서 사용해야한다.

 

클래스와 인터페이스 ( implements )

implements를 통해 클래스가 인터페이스를 따르도록 강제할 수 있다.

클래스가 반드시 인터페이스의 모든 메서드/속성을 구현해야만 컴파일이 통과한다.

예시를 통해 살펴보면,

interface Syncable {
  readonly id: string;     // 읽기 전용도 강제 가능
  sync(): Promise<void>;   // 비동기도 가능
}

class Account implements Syncable {
  constructor(public readonly id: string) {}
  
  async sync(): Promise<void> {
    // ... 서버 동기화 로직
    console.log("동기화 완료")
  }
}

// implements와 정적 멤버
interface Identifiable {
  id: string;
}

class User implements Identifiable {
  static table = "users"; // ✅ 인터페이스로는 검사 안 됨
  constructor(public id: string) {}
}
  • readonly / 옵셔널( ? ) 도 그대로 강제가 된다. readonly 인터페이스 필드는 getter 만으로도 만족시킬 수 있다.
  • 오버로드 시그니처가 있는 인터페이스를 구현할 때, 클래스 메서드는 오버로드 전체를 커버하는 구현이어야 한다.
  • implements 의 검사 대상은 인스턴스 멤버 뿐이다. 정적(static)멤버는 검사하지 않는다. (interface Identifiable 예시 참고)
Tip
공통 로직/상태 공유가 필요하면 abstract class가 낫다. 인터페이스는 타입 계약, 추상 클래스는 부분 구현 + 계약까지 제공한다.
// 추상 클래스 (abstact class)
// 계약 + 공통 로직을 한번에 제공할 수 있다.
// 여러 파생 클래스가 같은 상태나 메서드 구현을 공유해야 할 때는 interface보다 좋다.

abstract class BaseRepository<T> {
  protected items: T[] = [];

  abstract find(id: string): T | undefined; // 반드시 구현해야 함

  save(entity: T): void {
    this.items.push(entity); // 공통 구현
  }
}

class UserRepository extends BaseRepository<{ id: string; name: string }> {
  find(id: string) {
    return this.items.find(u => u.id === id);
  }
}

 

배열 인덱싱 타입 정의

배열처럼 인덱스 번호로 접근하는 경우, 인터페이스 안에 [index: number] 시그니처를 쓸 수 있다.

배열 요소의 타입을 명확히 보장한다.

interface StringArray {
  [index: number]: string;
}

let companies: StringArray = ['삼성', 'LG', '현대']

companies[0]; // 삼성

// 변경 불가한 배열이 필요하다면 ReadonlyArray<string>
const companies: ReadonlyArray<string> = ["삼성", "LG", "현대"];
// companies[0] = "기아"; // 에러

 

객체 인덱싱 타입 정의

정확한 속성 이름을 모를 때, 속성 이름의 타입과 속성 값의 타입을 지정할 수 있다.

속성이 동적으로 늘어날 수 있는 객체에 유용하다. ( 키-값 쌍으로 된 객체를 정의할 때 )

interface SalaryMap {
  [level: string]: string;
}

const salaries: SalaryMap = {
  junior: "3000만원",
  senior: "6000만원",
  lead: "1억"
};

 

키 집합이 제한적이라면 Record를 사용하는게 더 좋을 수 있다. 잘못된 키를 방지하고 자동 완성도 훨씬 좋아질 수 있다.

type Level = "junior" | "senior" | "lead";
type SalaryMap = Record<Level, string>;

const salaries: SalaryMap = {
  junior: "3000만원",
  senior: "6000만원",
  lead: "1억",
};

 

인덱스 시그니처

정확히 속성 이름을 명시하지 않고 속성 이름의 타입과 속성 값의 타입을 정의하는 문법

단순히 객체와 배열을 인덱싱할 때 활용될 뿐만 아니라 객체의 속성 타입을 유연하게 정의할 때도 사용된다.

interface SalaryMap {
  [level: string]: string;
}

// 정확한 속성 이름을 미리 알 수 없을때는 제네릭을 이용하면 좋다.
interface Dict<T = unknown> {
  [key: string]: T;
}

 


3. 타입 별칭 (type alias) vs 인터페이스 (interface) 차이와 사용처

타입 별칭과 인터페이스의 차이점

interface Animal { name: string }
interface Dog extends Animal { bark(): void }

type AnimalType = { name: string }
type DogType = AnimalType & { bark(): void }

바로 위의 AnimalType과 Animal Interface는 동일한 타입을 정의하고 "객체 정의" 자체만 보면 둘 다 가능하다.

그럼 둘의 차이는 무엇이고, 어떤 상황에서 어떻게 쓰는 것이 좋은지 좀 더 자세히 알아보자.

 

1) 확장성

타입 별칭과 인터페이스는 타입을 확장하는 방식이 다르다.

// interface 확장
interface Animal {
  name: string;
}
interface Dog extends Animal {
  bark(): void;
}

// type 확장
type AnimalType = { name: string };
type DogType = AnimalType & { bark(): void };

둘 다 확장은 가능하지만, interface는 상속 문법이 더 직관적이고, 라이브러리에서 다른 사람이 정의한 인터페이스에 내가 속성을 덧붙여 병합할 수도 있다. 

요약: 확장 방식의 차이
interface : 상속 (extends 키워드로 확장.)
type alias : (&) intersertion 타입으로 객체 타입 2개를 합쳐 사용

 

2) 선언 병합 (Declaration Merging)

interface Window {
  title: string;
}
interface Window {
  ts: TypeScriptAPI;
}

// 위 두 interface는 자동으로 합쳐져서

interface Window {
  title: string;
  ts: TypeScriptAPI;
}

인터페이스는 선언 병합이 가능하지만, 타입 별칭은 불가능하다.

즉, 같은 이름의 인터페이스를 여러 번 선언하면 해당 인터페이스들은 병합되어 하나의 인터페이스로 처리되지만, 타입 별칭은 중복 선언 시 에러가 발생한다. 그래서 전역 타입 확장에서 interface가 유리한 부분이 있다.

 

3) 표현력

type의 경우 모든 타입을 표현할 수 있다. (유니온, 튜플, 조건부 타입 등) 하지만 interface는 객체 모양만 표현할 수 있다.

인터페이스는 주로 객체의 타입을 정의하는 데 사용되고, 타입 별칭은 일반 타입에 이름을 짓는 데 사용하거나 유니언 타입, 인터섹션 타입 등에도 사용할 수 있다.

// type alias 가능
type Status = "pending" | "success" | "error";
type Point = [number, number];
type ApiResponse<T> = T extends { status: number } ? T : never;

// interface 불가능
interface Person {
  name: string;
  age: number;
}

type Adult = {
  old: boolean;
}

type Teacher = Person & Adult;

 

4) 런타임 호환성 (성능)

  • 공식 TS 팀 문서에 따르면 interface는 타입체커가 내부적으로 최적화를 좀 더 잘 할 수 있다고 한다.
  • 아주 미세한 차이지만, 대형 프로젝트에서는 interface 쪽이 더 빠르다고 알려져 있다.
    (물론 일반 규모 프로젝트에서는 체감하기 어렵다)

5) 언제 무엇을 쓰면 좋을까?

interface User {
  id: number;
}
interface User {
  name: string;
}
// 자동 병합 → { id: number; name: string }


type Status = "pending" | "success" | "error";
type Coordinate = [number, number];
type ApiResponse<T> = T extends { status: number } ? T : never;
  • Interface
    • 객체의 “설계도”를 정의할 때 (특히 클래스와 같이 사용할 때)
    • 다른 사람이 만든 타입을 보강(merge) 해야 할 때 (예: Express.Request에 user 추가)
    • 라이브러리나 SDK처럼 확장 가능성을 염두에 둘 때
  • type alias
    • 유니온("A" | "B")이나 튜플([number, number]) 같은 복잡한 타입을 다룰 때
    • 조건부 타입, 제네릭 변형 등 타입 레벨 로직을 쓸 때
    • 재사용과 조합을 중심으로 코드를 작성할 때

정리

  • type은 객체, 유니온, 튜플, 함수 타입 등 모든 타입의 별칭이 될 수 있다.
  • 하지만 인터페이스와 달리 declaration merging이 불가능하다.
  • 인터페이스는 객체 형태만을 설명할 수 있다.
  • 인터페이스는 다른 인터페이스를 통해 확장이 가능하다.

내 생각

  • 객체 지향 프로그래밍에서 상속이나 확장에 interface가 더 유리하므로 기본적으로는 interface를 많이 사용하고, 유니온 타입, 인터섹션 타입, 튜플 타입 등 복잡한 타입을 정의해야할 때는 type alias를 사용하는 것이 좋다고 생각한다.

4. Enum

  • 특정 값의 집합을 이름으로 정의할 때 사용하는 데이터 타입.
  • JavaScript에는 없지만 TypeScript에서 제공하는 전용 문법이다.
  • 코드에서 의미 있는 상수 집합을 정의할 때 사용한다.

1) 숫자형 Enum (Numeric Enum)

enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right  // 3
}

console.log(Direction.Up);     // 0
console.log(Direction[0]);     // 'Up'

 

  • 별도로 값을 지정하지 않으면 0부터 1씩 증가하는 숫자가 자동 할당된다.
  • 숫자형 이넘은 양방향 매핑을 지원한다.
    • 숫자 → 이름
    • 이름 → 숫자
  • 이 특징 때문에 디버깅할 때 직관적이지 않을 수 있다.

2) 문자형 Enum (String Enum)

enum Direction {
  Up = 'Up',
  Down = 'Down',
  Left = 'Left',
  Right = 'Right'
}

console.log(Direction.Up) // 'Up'
  • 값이 문자열로 고정된다.
  • 반드시 모든 속성에 직접 문자열 값을 지정해야 한다.
  • 숫자형과 달리 자동 증가 규칙은 없다.
  • 장점: 가독성이 좋고, 런타임에서도 값이 명확히 드러남.
  • 실무에서는 문자형 Enum을 더 선호하는 경우가 많다.

3) const Enum

const enum logLevel {
  Debug = 'Debug',
  Info = 'Info',
  Error = 'Error'
}
  • const를 붙이면 컴파일 타임에 인라인 처리된다.
  • 즉, 자바스크립트 결과물에서 별도의 객체를 만들지 않고 값만 직접 치환된다.
  • 장점: 결과 코드가 가볍고 빠르다.