인덱스 시그니처와 맵드 타입, 제네릭

김래영·2024년 11월 10일
0

TypeScript

목록 보기
4/4

인덱스 시그니처(Index Signatures)

  • 객체의 속성 이름이 미리 정의되지 않았을 때, 속성 이름이 동적으로 결정되는 경우 해당 속성의 키와 값을 지정하는 문법이다.
  • 인터페이스 내부에 [key: K]: T와 같이 타입을 명시하면 해당 타입의 속성 키는 모두 K타입이어야 하고 속성 값은 모두 T 타입을 가져야 한다는 의미이다.
  • 다양한 상황에서 동적 객체를 타입 안전하게 정의하는 데 유용하지만, 사용 시 미리 정의된 속성과의 타입 충돌에 주의해야 한다.

여러 속성을 동적으로 정의할 때 사용 예시

interface StringNumber {
  [key: string]: number;
};

const scores: StringNumber = {
  Alice: 90,
  Bob: 85,
  Charlie: 92,
};

배열처럼 사용할 때 사용 예시

  • 인덱스 시그니처는 배열과 같이 키가 숫자일 때도 사용할 수 있다.
interface NumberArray {
  [index: number]: string;
};

let myArray: NumberArray = ["Alice", "Bob", "Charlie"];
  • 인덱스 시그니처는 유연하지만, 객체에 미리 정의된 특정 속성과 함께 사용하면 제한이 있어 인덱스 시그니처에 포함되는 타입이어야 한다.
interface IndexSignatures {
  [key: string]: number | boolean;
  length: number;
  isValid: boolean;
  name: string; // 에러 발생
}

맵드 타입(Mapped Types)

  • 맵드 타입(Mapped Types)은 기존 타입의 속성을 반복(iterate)하면서 새로운 타입을 생성하는 방법이다.
  • keyofin을 사용해 타입의 키들을 순회하며 각 속성에 대한 새로운 정의를 할 수 있다.
  • 맵드 타입을 통해 속성을 optional 또는 readonly로 만들거나, 선택적 속성을 필수로 변환할 수 있다.
  • TypeScript에서 기본 제공하는 Partial, Readonly, Required, Pick 등도 맵드 타입을 기반으로 동작한다.
  • 주어진 타입의 모든 속성 키를 순회하고 그 속성들의 타입을 변경하거나 optional, readonly 등의 제약을 추가할 수 있다.
  • 맵드 타입은 코드의 재사용성을 높이고, 기존 객체 타입을 다양하게 변형할 수 있다.
type Example = {
  a: number;
  b: string;
  c: boolean;
};

type Subset<T> = {
  [K in keyof T]?: T[K];
};

const exampleSubset: Subset<Example> = { a: 3 };
  • P in keyof OldType: OldType의 모든 속성 이름을 하나씩 가져온다.
  • NewTypeForP: 각 속성의 타입을 지정하거나 변형한다.
  • 맵드 타입에서 매핑할 때 readonly와 ?를 수식어로 적용할 수 있다.
  • 기존 타입에 존재하던 readonly와 ?앞에 -를 붙여주면 해당 수식어를 제거한 타입을 선언할 수 있다.
type NewType = {
  [P in keyof OldType]: NewTypeForP;
};
type ReadOnly<T> = {
  readonly [P in keyof T]: T[P];
};

type ReadOnlyExample<T> = {
  -readonly [P in keyof T]: T[P];
};

type Optional<T> = {
  [P in keyof T]?: T[P];
};

type OptionalExample<T> = {
  [P in keyof T]-?: T[P];
};

유틸리티 타입 구현 예시

Partial<T> 직접 구현

TypeScript에서 제공하는 Partial<T>는 기존 객체 타입의 모든 속성을 선택적(optional)으로 만드는 제네릭 타입이다.

type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

Required<T> 직접 구현

Required<T>는 객체 타입의 모든 속성을 필수적(required)으로 만드는 제네릭 타입이다.

type MyRequired<T> = {
  [P in keyof T]-?: T[P]
};

Readonly<T> 직접 구현

Readonly<T>는 객체 타입의 모든 속성을 읽기 전용(readonly)으로 만드는 제네릭 타입이다.

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
};

Pick<T, K> 직접 구현

Pick<T, K>는 주어진 타입 T에서 속성 K만을 선택한 새로운 타입을 만드는 제네릭 타입이다.

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

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

type PickPerson = MyPick<Person, 'name' | 'city'>; // 'name'과 'city'만을 선택
  • extends는 TypeScript에서 제약 조건을 설정할 때 사용된다. K extends keyof T는 타입 K가 반드시 T의 속성 키들 중 하나여야 한다는 조건을 설정하는 것이다.

키워드 정리

keyof

  • keyof는 타입의 모든 키를 추출하는 데 사용되며 타입의 속성 이름들을 유니언 타입으로 가져오는 역할을 한다.
type Person = {  
  name: string;  
  age: number;  
};

type PersonKeys = keyof Person; // 'name' | 'age'

typeof

  • typeof는 JavaScript나 TypeScript에서 값의 타입을 추론하는 데 사용되며 기본적으로는 값의 타입을 가져오는 역할을 한다.
const myObject = {  
  name: "Alice",  
  age: 30  
};

type MyObjectType = typeof myObject;

keyof typeof의 결합

  • keyof typeof를 함께 사용하면 객체의 타입을 먼저 추론한 다음 그 타입의 속성 키들을 가져오는 방법이다.
const person = {  
  name: "Alice",  
  age: 30,  
  job: "Engineer"  
};

type PersonKeys = keyof typeof person;

in

  • 맵핑된 타입에서 순회(iteration)를 나타낸다. in을 사용하면 특정 타입이나 유니언의 각 값을 하나씩 가져와 새로운 타입을 정의할 수 있다.

in keyof

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

// 맵핑된 타입을 사용해 Person의 모든 속성을 optional로 만들기
type PartialPerson = {
  [P in keyof Person]?: Person[P];
};

템플릿 리터럴 타입(Template Literal Type)

  • 자바스크립트의 템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법이다.
type State = 'on' | 'off';

type StateName = `${State}-swich`;  // 'on-swich' | 'off-swich'

제네릭(Generic)

  • 제네릭은 정적 언어에서 다양한 타입 간에 재사용성을 높이기 위해 사용하는 문법으로 여러 타입을 처리할 수 있는 재사용 가능한 함수, 클래스, 인터페이스를 정의할 때 사용된다.
  • 제네릭을 사용하면 타입에 의존하지 않고 다양한 데이터 타입에서 동작하는 코드를 작성할 수 있다.
  • 코드의 재사용성과 타입 안전성을 보장한다.
  • 타입 변수는 일반적으로 와 같이 꺽쇠괄호 내부에 정의되며, 사용할 때 함수에 매개변수를 넣는 것과 유사하게 원하는 타입을 넣어주면 된다.
    • T(Type), E(Element), K(Key), V(Value) 등 한 글자로 된 이름을 많이 사용한다.
  • 타입을 명시하는 부분을 생략하면 컴파일러가 인수를 보고 타입을 추론해 준다. 타입 추론이 가능한 경우에는 타입 명시를 생략할 수 있다.
function identity<T>(arg: T): T {
  return arg;
};

identity<string>("Hello"); // "Hello"가 반환되고 T는 string이 된다.
identity<number>(42);       // 42가 반환되고 T는 number가 된다.
identity("Hello");  // TypeScript가 T를 string으로 추론한다.
identity(42);       // TypeScript가 T를 number로 추론한다.

제네릭 제약 조건 설정하기

  • extends 키워드를 사용해 제네릭 타입에 제약을 추가할 수 있다.
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
};

getLength("Hello");        // 정상, string은 length 속성을 가짐
getLength([1, 2, 3]);      // 정상, 배열은 length 속성을 가짐
getLength({ length: 10 }); // 정상, 객체에 length 속성이 있음
getLength(42);             // 에러, number에는 length 속성이 없음
  • 여기서 T extends { length: number }T는 반드시 length 속성을 가져야 한다는 제약을 의미한다.
  • T는 배열, 문자열, 객체 등 length 속성을 가진 타입만 사용할 수 있다.

제네릭 타입의 기본값 설정하기

  • 제네릭 타입을 명시적으로 지정받지 않았을 때 기본값을 사용할 수 있도록 설정할 수 있다.
function identityWithDefault<T = string>(arg: T): T {
  return arg;
};

let defaultString = identityWithDefault(10); // T는 number로 추론됨
let defaultIdentity = identityWithDefault("Hello"); // T는 string (기본값)
profile
개발 노트

0개의 댓글