Typescript

객체의 속성과 메서드에 적용되는 특징을 알자

비숑주인 2025. 3. 22. 16:25

TypeScript는 객체의 구조를 엄격히 관리할 수 있도록 다양한 문법과 기능을 제공한다. 이번 글에서는 객체의 속성과 메서드에 적용할 수 있는 주요 특징들을 중심으로 살펴보자. 이는 interface나 type alias 중 무엇을 쓰든 공통적으로 적용되는 개념이다. 

 

Optional과 Readonly 수식어

객체의 속성에는 ?를 붙여 옵셔널(optional)로 만들거나, readonly를 붙여 읽기 전용(read-only) 으로 만들 수 있다.

 

interface Example {
  hello: string;
  world?: number;           // 옵셔널
  readonly wow: boolean;    // 읽기 전용
  readonly multiple?: symbol; // 읽기 전용 + 옵셔널
}
const example: Example = {
  hello: 'hi',
  wow: false,
};

example.world = 42;       // 가능 (옵셔널이라 추가해도 됨)
example.wow = true;       // ❌ Error: read-only 속성은 수정 불가

 

world는 있어도 되고 없어도 되는 속성이므로 undefined도 허용된다. 

 

객체 리터럴 vs 변수 할당의 타입 검사

TypeScript는 객체 리터럴을 직접 대입할 때와 변수를 통해 간접적으로 대입할 때 타입 검사를 다르게 수행한다. 

interface Example {
  hello: string;
}

const example1: Example = {
  hello: 'hi',
  why: 'unexpected', // ❌ Error: 'why'는 정의되지 않은 속성
};

const obj = {
  hello: 'hi',
  why: 'ok',
};

const example2: Example = obj; // ✅ 통과됨

 

왜 이런 차이가 생길까?

객체 리터럴을 직접 대입하면 잉여 속성 검사(Excess Property Checking) 가 실행되어 선언되지 않은 속성은 에러로 처리된다. 반면 변수를 통해 대입하면 호환성 검사만 수행되어 잉여 속성이 허용된다. 실무에서는 객체 리터럴로 직접 넘길 때 타입 오류가 발생하면, 중간 변수로 분리해 확인할 수도 있다. 

 

함수에서도 발생하는 잉여 속성 검사

interface Money {
  amount: number;
  unit: string;
}

function addMoney(a: Money, b: Money): Money {
  return { amount: a.amount + b.amount, unit: 'won' };
}

addMoney({ amount: 1000, unit: 'won', error: 'oops' }, { amount: 2000, unit: 'won' });
// ❌ Error: 'error'는 정의되지 않은 속성

 

마찬가지로 객체 리터럴을 직접 함수 인자로 넘기면 에러가 발생하지만, 변수를 통해 넘기면 통과 된다. 

 

구조 분해 할당 시 올바른 타입 표기

const { prop: { nested } }: { prop: { nested: string } } = {
  prop: { nested: 'hi', a: 1, b: true },
};
console.log(nested); // 'hi'

 

구조 분해 할당에서 변수명과 타입명을 혼동하지 않도록 주의해야 하자. 

 

인덱스 접근 타입 (Indexed Access Type)

type Animal = {
  name: string;
  age: number;
};

type NameType = Animal['name']; // string

 

속성의 타입을 재사용하거나 연동하고 싶을 때 유용하다. 객체처럼 'key' 또는 "key"로 접근 가능하지만, Animal.name처럼 점 표기법은 사용할 수 없다. 

 

keyof 연산자

const obj = {
  hello: 'world',
  name: 'zero',
  age: 28,
};

type Keys = keyof typeof obj;      // 'hello' | 'name' | 'age'
type Values = typeof obj[Keys];    // string | number

 

 

  • keyof는 객체의 키 타입 유니언을 생성
  • typeof는 변수의 타입을 추론
  • typeof obj[Keys]로 값 타입 유니언을 생성 가능

 

메서드 선언 방식 세 가지

interface Example {
  a(): void;
  b: () => void;
  c: {
    (): void;
  };
}

 

 

매핑된 객체 타입 (Mapped Object Type)

인덱스 시그니처의 한계를 매핑된 타입으로 해결 할 수 있다. 리터럴 유니언 타입은 인덱스 시그니처에서 사용할 수 없다. 

type HelloAndHi = {
  [key: 'hello' | 'hi']: string; // ❌ Error
};

 

type HelloAndHi = {
  [key in 'hello' | 'hi']: string;
};
// 결과: { hello: string; hi: string; }

 

기존 타입 복사하기

interface Original {
  name: string;
  age: number;
  married: boolean;
}

type Copy = {
  [key in keyof Original]: Original[key];
};

 
TS에서 기존 객체 타입을 복사할 수 있으며, 수식어도 추가하거나 제거할 수 있다. 

// 수식어 추가
type ReadOnlyOptional = {
  readonly [key in keyof Original]?: Original[key];
};

// 수식어 제거
type Cleaned = {
  -readonly [key in keyof ReadOnlyOptional]-?: ReadOnlyOptional[key];
};

 

속성 이름 변경

type Copy = {
  [key in keyof Original as Capitalize<key>]: Original[key];
};
// 결과: { Name: string; Age: number; Married: boolean }

 

as와 Capitalize, Uncapitalize, Uppercase, Lowercase 같은 유틸리티 타입을 조합하면 객체의 키 이름도 변경 가능하다. 

 

개념 설명
? 속성을 옵셔널로 만듦
readonly 속성을 읽기 전용으로 만듦
keyof 객체의 키 유니언 타입 추출
인덱스 접근 타입 속성의 값 타입을 추출
잉여 속성 검사 객체 리터럴에만 적용되는 추가 속성 검사
매핑된 타입 기존 타입 기반으로 새로운 타입 생성
수식어 제거/추가 -readonly, +readonly
속성 이름 변경 as Capitalize<key> 등 활용