📙

9장 타입 조작

9.1 인덱스 시그니처 (Index Signature)

9.1.1 인덱스 시그니처란?

타입스크립트의 인덱스 시그니처는 객체의 인덱스 타입과 해당 인덱스에 해당하는 값의 타입을 정의하는 방식입니다. 이를 통해 객체의 프로퍼티 이름을 사전에 알 수 없거나 동적으로 변경될 수 있는 경우에도 타입의 안전성 보장이 가능합니다. 인덱스 시그니처는 다음과 같은 형태로 표현됩니다.
interface StringArray { [index: number]: string; }
위의 코드에서 index는 인덱스의 이름이며, 이는 임의로 변경할 수 있습니다. number는 인덱스의 타입을 의미하며, string은 해당 인덱스에 할당될 값의 타입을 의미합니다. 이 예제에서 StringArray는 숫자 인덱스에 문자열 값을 가지는 것을 의미합니다.

9.1.2 인덱스 시그니처의 허용 타입

타입스크립트에서 인덱스 시그니처에 허용되는 인덱스 타입은 string, number, 템플릿 문자열입니다.

1) string 인덱스 시그니처

string 인덱스 시그니처를 사용하면 임의의 문자열을 키로 사용할 수 있습니다.
interface StringDictionary { [key: string]: string; } let refDictionary: StringDictionary = {}; refDictionary["first"] = "Hello"; refDictionary["second"] = "World"; console.log(refDictionary["first"]); // Hello console.log(refDictionary["second"]); // World
StringDictionary는 문자열 키에 문자열 값을 가지는 인터페이스입니다. refDictionary 객체는 이 인터페이스를 구현하므로, 문자열을 키로 사용하여 문자열 값을 저장할 수 있습니다.

2) number 인덱스 시그니처

number 인덱스 시그니처를 사용하면 숫자를 키로 사용할 수 있습니다.
interface NumberDictionary { [index: number]: string; } let refDictionary: NumberDictionary = {}; refDictionary[0] = "zero"; refDictionary[1] = "one"; refDictionary[2] = "two"; console.log(refDictionary[0]); // zero console.log(refDictionary[1]); // one console.log(refDictionary[2]); // two
NumberDictionary는 숫자 인덱스에 문자열 값을 가지는 인터페이스입니다. 따라서 refDictionary 객체는 숫자를 키로 사용하여 문자열 값을 저장할 수 있습니다.

3) 템플릿 문자열 인덱스 시그니처

타입스크립트 4.1 버전부터 템플릿 문자열을 인덱스 시그니처의 키로 사용할 수 있습니다. 이 경우 :이 아닌 in을 사용해 명시합니다.
interface TemplateStringIndex { [K in `key${number}`]?: string; } let refObj: TemplateStringIndex = {}; refObj["key1"] = "Value 1";
TemplateStringIndex라는 인터페이스는 템플릿 문자열 타입인 key${number}을 인덱스 시그니처의 키로 사용하고 있습니다. 즉, key1, key2, key3, ... 형태의 키를 가질 수 있습니다. 각 키에는 문자열 타입의 값이 할당될 수 있습니다. 이와 같이 템플릿 문자열 인덱스 시그니처를 사용하면 동적으로 변하는 키를 가진 책체를 다룰 수 있습니다.
 
💡
더 자세히 알아보기 🤓 1️⃣ string 인덱스 시그니처와 number 인덱스 시그니처의 동시 사용 시 주의 사항
문자열 인덱스 시그니처와 숫자 인덱스 시그니처를 동시에 사용하는 경우, 숫자 인덱스 시그니처의 반환 값은 항상 문자열 인덱스 시그니처의 반환 값의 하위 타입이어야 합니다. 이는 타입스크립트가 숫자로 인덱싱을 시도한 후, 실패하면 문자열로 인덱싱을 시도하기 때문입니다.
interface StringNumberDictionary { [index: string]: string; [index: number]: string; // OK } interface StringNumberDictionary2 { [index: string]: string; [index: number]: number; // Error // "숫자 인덱스 타입 'number'는 문자열 인덱스 타입 'string'에 할당될 수 없습니다." }
 
2️⃣ symbol 타입을 인덱스 시그니처의 키로 사용하기
타입스크립트에서는 symbol 타입을 인덱스 시그니처의 키로 직접 사용하는 것을 지원하지 않으나 symbol로 객체 프로퍼티를 생성하고 접근하는 것은 가능합니다.
const key = Symbol("key"); let refObj = { [key]: "1234" }; console.log(refObj[key]); // "1234"
위의 코드에서 Symbol 함수를 사용하여 새로운 심볼을 생성하고, 이를 key라는 상수에 할당하고 있습니다. Symbol은 자바스크립트의 원시 타입 중 하나로, 고유하고 변경 불가능한 값을 나타냅니다. refObj라는 객체는 key 심볼을 프로퍼티 키로 사용하여 "1234"라는 값을 가지고 있습니다. 이 프로퍼티에 접근하기 위해서는 대괄호 표기법을 사용해야 합니다.
 
3️⃣ 유니온 타입을 인덱스 시그니처의 키로 사용할 수는 없을까? string, number, symbol, 템플릿 문자열을 조합한 유니온 타입을 인덱스 시그니처의 키로 사용하는 것은 지원되지 않습니다. 유니온 타입을 값으로 사용하는 것은 가능합니다.
interface UnionValueIndex { [index: string]: string | number; } let myObj: UnionValueIndex = {}; myObj["test"] = "Test String"; myObj["example"] = 123;

9.1.3 인덱스 시그니처의 확장

타입스크립트의 인덱스 시그니처는 확장 가능합니다. 하나의 인덱스 시그니처를 가진 인터페이스나 클래스를 정의하고, 다른 인터페이스나 클래스에서 그 인터페이스나 클래스를 확장하여 사용할 수 있습니다.
interface Parent { [index: string]: string | number; name: string; } interface Child extends Parent { age: number; } let myInfo: Child = { name: "Duke", age: 30 };
Parent는 문자열 인덱스에 string 또는 number 값을 가지는 인덱스 시그니처를 가집니다. 그리고 ChildParent를 확장하여 age라는 추가적인 프로터티를 가질 수 있도록 확장하였습니다.
이처럼 인덱스 시그니처는 인터페이스와 클래스의 확장을 통해 더 복잡한 타입을 만들 수 있게 해줍니다. 단, 확장 시에는 원래 인덱스 시그니처의 제약사항을 준수해야 합니다. 예를 들어, 문자열 인덱스 시그니처를 가진 인터페이스를 확장하는 경우, 확장된 인덱스 시그니처도 문자열 인덱스를 가져야 합니다.

9.1.4 인덱스 시그니처의 readonly

인덱스 시그니처는 readonly를 사용하여 읽기 전용으로 만들 수 있습니다. 이를 통해 인덱스에 대한 할당을 방지할 수 있습니다.
interface ReadonlyStringArray { readonly [index: number]: string; } let refArray: ReadonlyStringArray = ["Duke", "Bella"]; refArray[1] = "Alice"; // Error: readonly로 컴파일 에러가 발생합니다.
ReadonlyStringArray는 인덱스 시그니처를 사용하여 숫자 인덱스를 가지고 문자열 값을 반환하는 배열 타입을 정의합니다. readonly 키워드는 해당 인덱스의 값을 변경할 수 없게 만듭니다. 즉, 배열의 요소를 변경하려고 하면 컴파일 시점에 에러를 발생시킵니다.
refArray 변수는 ReadonlyStringArray 타입으로 선언, ["Duke", "Bella"]로 초기화되었습니다. 이 배열은 읽기 전용으로, 한 번 생성된 후에는 요소를 변경할 수 없습니다. 따라서 refArray[1]를 Alice로 변경하려고 시도할 경우 컴파일 에러가 발생하게 됩니다.
 

9.2 인덱스드 액세스

타입스크립트의 인덱스드 엑세스 타입(Indexed Access Type)은 T[K] 형태로 표현되며, 이를 사용해 특정 타입 T의 특정 프로퍼티 또는 요소의 타입을 추출할 수 있습니다. 여기서 T는 어떤 타입이고, K는 그 타입의 인덱스를 나타냅니다.
인덱스는 객체의 프로퍼티 이름 또는 배열, 튜플의 요소 위치를 의미합니다. 즉, T[K]에서 KT 타입에서 특정 속성의 타입을 참조하거나, 배열이나 튜플의 특정 위치의 요소 타입을 참조하는 역할을 합니다.
따라서 T[K]를 사용하면 객체 타입에서 특정 프로퍼티의 타입을, 배열이나 튜플 타입에서 특정 위치의 요소 타입을 쉽게 추출할 수 있습니다.

9.2.1 객체 예시

객체 타입 하나와 해당 객체 타입을 갖는 변수를 하나 만들겠습니다.
interface Animal { name: string; species: string; description: { color: string[]; diet: string; weight: number; }; } let animal: Animal = { name: "푸바오", species: "판다", description: { color: ["검은색", "흰색"], diet: "채식", weight: 110, }, };
Animal 인터페이스는 동물의 정보를 나타냅니다. 이 인터페이스는 동물의 이름(name), 종(species), 그리고 상세 정보(description)를 프로퍼티로 가지고 있습니다. 상세 정보는 동물의 색상(color), 식성(diet), 그리고 무게(weight)를 포함합니다.
animal 객체는 Animal 인터페이스를 따르며, ‘푸바오’라는 이름의 ‘판다’ 종을 나타냅니다. 이 동물은 검은색과 흰색을 가지고 있고, 채식을 하며, 무게는 110kg입니다.
이제 객체의 description 프로퍼티에서 식성과 몸무게를 출력하는 함수를 추가하겠습니다.
원래 알던 대로 한다면 객체 리터럴 타입으로 만들 수 있을 것입니다.
function introduceAnimal(description:{ color: string[]; diet: string; weight: number; }) { console.log( `${description.diet}이며, 몸무게는 ${description.weight}kg입니다.` ); }
이런 방식으로 타입을 정의하는데 문제가 발생하지는 않습니다. 그러나, 만약에 animal 타입에서 description 프로퍼티에 형제, 자매를 포함하라는 요구사항이 제시된다면 새로운 프로퍼티인 siblings를 생성해야 합니다.
interface Animal2 { name: string; species: string; description: { color: string[]; diet: string; weight: number; siblings: string[]; //추가 }; } let animal2: Animal2 = { name: "푸바오", species: "판다", description: { color: ["검은색", "흰색"], diet: "채식", weight: 110, siblings: ["후이바오", "루이바오"], //추가 }, }; function introduceAnimal(description:{ color: string[]; diet: string; weight: number; siblings: string[]; //추가 }) { console.log( `${description.diet}이며, 몸무게는 ${description.weight}kg입니다.` ); }
현재 예시에서는 함수가 단 하나뿐이라 간단히 추가할 수 있지만, 객체 매개변수를 받는 함수가 여러 개일 경우에는 비효율적이고 복잡한 상황이 발생할 수 있습니다.
이러한 문제를 해결하기 위해서는 좀 더 효과적인 방법인 ‘인덱스드 엑세스 타입’을 활용하는 것이 좋습니다.
function introduceAnimal(description: Animal["description"]) { console.log( `${description.diet}이며, 몸무게는 ${description.weight}kg입니다.` ); }
인덱스드 엑세스 타입은 새로운 프로퍼티가 추가되거나 기존 프로퍼티 타입이 변경되었을 때, 이를 즉시 반영해 줍니다. 이는 원본 타입이 바뀌더라도 별도로 추가 작업을 하지 않아도 되므로, 사용자에게 매우 편리하다는 장점이 있습니다.
타입스크립트의 인덱스드 엑세스 타입을 사용하면 중첩된 프로퍼티의 타입을 추출할 수 있습니다. 예를 들어 description 프로퍼티 내부의 color 프로퍼티의 타입을 가져오려면, 먼저 description프로퍼티의 타입을 추출합니다. 그 후, 이 객체 타입에서 color 프로퍼티의 타입을 다시 추출하면 됩니다. 이렇게 중첩된 프로퍼티에 대해서도 인덱스드 엑세스 타입을 활용할 수 있습니다.
function printAnimalColor(color: Animal2["description"]["color"]) { console.log(`이 동물은 ${color}입니다.`); }

1) 주의할 점

인덱스 액세스 타입의 객체를 사용할 때는 몇 가지 주의 사항이 있습니다. 첫 번째는 인덱스에 값이 아닌 ‘타입’만 사용할 수 있다는 것입니다. 아래 코드를 보시면, description이라는 문자열 값을 다른 변수에 저장하고 인덱스로 사용하려 하면 오류가 발생합니다.
const desciptionKey = "description"; function introduceAnimal(description: Animal[descriptionKey]) { console.log( `${description.diet}이며, 몸무게는 ${description.weight}kg입니다.` ); } //Error
이 문제의 원인은 인덱스 부분에는 오직 '타입'만 사용할 수 있다는 점입니다. 그러나 descriptionKey는 값을 나타내는 역할을 하므로 이렇게 사용할 수 없습니다. 따라서 이 부분에는 문자열 리터럴 타입과 같은 타입만 사용할 수 있습니다.
두 번째는, 인덱스에 타입을 적을 때 존재하지 않는 프로퍼티를 쓰면 오류가 발생한다는 것입니다. 예를 들어, Animal 타입에 whatever라는 프로퍼티가 없다면 오류가 발생하게 됩니다. 이는 사실 당연한 오류입니다.
function introduceAnimal(description: Animal["whatever"]) { console.log( `${description.diet}이며, 몸무게는 ${description.weight}kg입니다.` ); } //Error

9.2.2 배열 예시

인덱스 액세스 타입을 사용할 때, 대괄호 안에 number를 넣으면 해당 배열 타입의 요소 타입을 추출할 수 있습니다. 이때 number는 타입을 나타내는 키워드로 사용되며, 실제 숫자 값을 의미하지는 않습니다.
type MovieList = { title: string; genre: string; premiere: number; information: { cast: string[]; ott: boolean; cinema: boolean; }; }[];
위의 코드의 MovieList는 영화 정보를 담은 객체의 배열입니다. 각 영화 정보 객체에는 제목(title), 장르(genre), 개봉일(premiere)이라는 기본 정보와 추가 정보(information)라는 객체가 포함되어 있습니다. 추가 정보(information) 객체 내에는 배우 목록(cast), OTT 서비스 상영 여부(ott), 영화관 상영 여부(cinema)를 표시합니다.
const firstmovie: MovieList[number] = { title: "웡카", genre: "판타지", premiere: 20240131, information: { cast: ["티모시 샬라메", "휴 그랜트", "칼라 레인"], ott: false, cinema: true, }, }; const secondmovie: MovieList[0] = { title: "듄2", genre: "액션", premiere: 20240228, information: { cast: ["티모시 샬라메", "젠데이아 콜먼", "레베카 퍼거슨"], ott: false, cinema: true, }, };
firstmoviesecondmovie라는 두 개의 상수는 각각 MovieList[number]MovieList[0] 타입을 가지고 있습니다. 여기서 number0MovieList 배열의 각 요소의 타입을 나타내기 위한 인덱스입니다. 따라서, firstmoviesecondmovieMovieList 배열의 각 요소인 영화 정보 객체의 타입을 가져야 합니다.
MovieList[number]라는 표현은 ‘MovieList 배열의 모든 요소의 타입을 표현한다'는 의미입니다. 여기서 number는 배열의 인덱스를 가리키는 것이 아니라, 배열의 모든 요소의 타입을 나타내는 키워드로 사용됩니다.
비슷하게, 대괄호 안에 숫자 리터럴을 직접 넣어서 MovieList[0] 같이 표현하면, 이 역시 MovieList 배열의 각 요소의 타입을 나타내게 됩니다. 이때 0은 실제 배열의 인덱스를 가리키는 것이 아니라, 타입을 표현하는 숫자 리터럴로 사용됩니다. 따라서 타입스크립트에서 이런 인덱스 액세스 타입을 사용하면, 대괄호 안의 숫자는 배열의 인덱스가 아닌, 배열 요소의 타입을 나타내는 역할을 합니다.
function printCastInfo1(information: MovieList[number]["information"]) { console.log(`이 영화에는 ${information.cast}이 출연합니다.`); } function printCastInfo2(cast: MovieList[number]["information"]["cast"]) { console.log(`이 영화에는 ${cast}이 출연합니다.`); }
마지막으로, printCastInfo1printCastInfo2라는 두 개의 함수는 영화 정보의 캐스트를 출력하는 역할을 합니다. 이 함수들은 각각 information 객체와 cast 배열을 매개변수로 받습니다. 이때 매개변수의 타입은 인덱스 액세스 타입을 사용하여 MovieList[number]["information"]MovieList[number]["information"]["cast"]로 지정되어 있습니다.

9.2.3 튜플 예시

type Tup = [number, string, boolean]; type Tup0 = Tup[0]; // number type Tup1 = Tup[1]; // string type Tup2 = Tup[2]; // boolean type Tup3 = Tup[3]; //Error: 길이가 '3'인 튜플 형식 'Tup'의 인덱스 '3'에 요소가 없습니다. type Tup4 = Tup[number]; // number | string | boolean
이번에는 숫자, 문자열, 불리언 타입을 가진 튜플 하나를 만들었습니다.
이 튜플에서 0번 인덱스는 숫자 타입, 1번 인덱스는 문자열 타입, 2번 인덱스는 불리언 타입이 됩니다. 이렇게 각 인덱스에 해당하는 타입을 추출할 수 있습니다. 그런데 튜플은 길이가 고정된 배열이기 때문에, 존재하지 않는 인덱스의 타입을 확인하려고 하면 오류가 발생합니다. 그리고 배열 타입을 추출할 때처럼, [number]를 사용하여 튜플의 모든 타입을 추출할 수도 있습니다. 이 튜플에는 문자열, 숫자, 불리언 타입이 모두 포함되어 있으므로, 이 세 가지 타입의 유니온 타입을 추출하게 됩니다.
 

9.3 Mapped Types

Mapped Types는 기존 타입의 각 프로퍼티를 변환하거나 새로운 프로퍼티를 추가하는 데 사용됩니다. 이 타입 조작은 주어진 기존 타입의 각 프로퍼티를 반복하고 각각에 대해 변환을 적용하여 새로운 타입을 만들 수 있도록 합니다.

9.3.1 자바스크립트의 map 함수

Mapped Types의 이름에서 보다시피 어원이 자바스크립트의 map 함수로부터 왔음을 짐작할 수 있습니다. map 함수는 배열의 각 요소에 대해 주어진 함수를 호출하고 그 결과를 새 배열에 담아 반환합니다.
자바스크립트의 map 함수의 일반적인 형태는 다음과 같습니다.
const newArray = array.map(callback(currentValue, index, array), thisArg);
예를 들어, string 타입인 배열의 각 요소를 숫자로 맵핑하여 새로운 배열을 반환해 보겠습니다.
const newArray = ['1', '2', '3'].map((value) => Number(value)); // newArray: [1, 2, 3]
위 코드에서는 map 함수를 통해 배열의 각 요소를 순회하면서 원하는 새 요소로 변환해 새로운 배열을 만들어낸다는 것을 알 수 있습니다. 이와 비슷한 원리로 타입스크립트는 타입을 다루는 언어이니 Mapped Type은 위의 로직에서 배열이 아닌 타입을 다룬다고 생각하면 됩니다.

9.3.2 Mapped Types 기본 문법

앞서 설명한 바와 같이 Mapped Type은 기존에 정의 되어 있는 타입을 가져와 각 프로퍼티에 변환을 적용하여 새로운 타입으로 변환하는 문법입니다.
Mapped Type은 일반적으로 다음과 같은 형태를 가집니다.
type NewType = { [K in KeysType]: ValueType };
KeysType 매핑에 사용되는 프로퍼티 이름의 타입을 지정하고,ValueType은 프로퍼티에 할당된 값의 새로운 타입을 지정합니다. K는 각 반복에서 할당해 주는 프로퍼티 이름을 나타내는 변수입니다.

1) 예시 코드

위의 설명이 이해되지 않을 수 있어 가장 기본적인 예시로 설명해 보겠습니다. 타입 Fruits가 다음과 같이 유니온 타입으로 선언되어 있다고 가정하겠습니다:
type Fruits = 'Apple' | 'Banana' | 'Cherry';
이제 각 과일에 해당하는 가격을 포함한 객체를 만들어 보겠습니다.
type FruitsInfo = { [K in Fruits]: number }; const FruitsList: FruitsInfo = { Apple: 3000, Banana: 2000, Cherry: 5000, };
이 코드에서는 각 과일에 대한 가격을 포함한 새로운 객체를 생성합니다. 여기서 Mapped Type의 기능을 사용하여, Fruits에 정의된 각 과일을 순회하고 해당 과일을 키로 가지는 프로퍼티를 추가하여 새로운 객체를 만들고 있습니다. 이것이 Mapped Type의 기본적인 동작 방식입니다. 하지만 아직 정확한 동작원리는 이해하기 어려우니 Mapped Type이 사용한 여러 가지 문법을 바탕으로 설명해 보겠습니다.

2) Index Signature

Mapped Types는 9.1장에서 소개한 인덱스 시그니처의 개념을 기반으로 동작합니다. 이전 예제의 FruitsInfo로 인덱스 시그니처를 살펴보겠습니다.
type FruitsInfo = { [K: string]: number };
인덱스 시그니처는 객체에서 프로퍼티에 대한 타입을 선언하는데 사용됩니다. 여기서 [K: string]은 객체의 모든 string 타입 키에 대해 해당하는 값을 number로 정의한다는 의미입니다.

3) in

in과 객체를 연관 지어 생각해 보면 자바스크립트의 for in 문법이 떠오를 것입니다. for in과 같이 in은 Mapped Type에서 주어진 유니온 타입(Fruits)을 반복하여 각 요소에 대해 작업을 수행하는 역할을 합니다.
type FruitsInfo = { [K in Fruits]: number };
위의 코드에서 in FruitsFruits 유니온 타입을 순회하며 각 과일에 대한 작업을 수행함을 의미합니다. 따라서 FruitsInfo 타입은 Fruits의 각 요소를 key 값으로 하고 number 타입의 value 값을 갖는 객체임을 알 수 있습니다.

4) keyof

예시 코드의 Fruits가 유니온 타입이 아닌 경우 keyof를 사용하면 주어진 타입의 모든 프로퍼티 키를 유니온 타입으로 추출할 수 있습니다.
type Fruits = { Apple: string; Banana: string; Cherry: string; }; // keyof로 'Apple' | 'Banana' | 'Cherry'로 변환 type FruitsInfo = { [K in keyof Fruits]: number }; const FruitsList: FruitsInfo = { Apple: 3000, Banana: 2000, Cherry: 5000, };
즉 Mapped Types를 정의할 때, 기존 타입의 프로퍼티 키를 반복하여 사용해야 할 때 keyof를 사용합니다. keyof에 대해 자세한 설명은 9.4장에서 이어서 하겠습니다.

9.3.3 Mapped Types 응용

1) 읽기 전용 프로퍼티

다음과 같은 파일 정보가 있을 때, 파일의 정보에는 읽기와 쓰기가 모두 가능한 상태입니다.
interface FileData { fileName: string; createDate: string; fileSize: number; } // 파일 정보 생성 const fileInfo: FileData = { fileName: 'example.txt', createDate: '2024-03-01', fileSize: 1024, }; // 파일 정보 출력 console.log('File Name:', fileInfo.fileName); console.log('Create Date:', fileInfo.createDate); console.log('File Size:', fileInfo.fileSize); // 파일 정보 수정 fileInfo.fileName = 'updated.txt'; fileInfo.createDate = '2024-03-03'; fileInfo.fileSize = 2048; // 수정된 파일 정보 출력 console.log('Updated File Name:', fileInfo.fileName); console.log('Updated Create Date:', fileInfo.createDate); console.log('Updated File Size:', fileInfo.fileSize);
파일 이름은 수정이 가능하지만 파일 생성일과 파일 사이즈를 변경하는 것은 위험하기 때문에 읽기 전용으로 만들어야 합니다.
interface ReadonlyFileData { fileName: string; readonly createDate: string; readonly fileSize: number; }
이러한 인터페이스를 만드는 것이 목표인데 FileDataReadonlyFileData 간에는 중복되는 코드 구조가 있어 수정이 필요합니다. 따라서 FileData를 통해 파일 생성일과 파일 사이즈를 readonly 속성으로 변경하는 Mapped Type을 정의하겠습니다.
interface FileData { fileName: string; createDate: string; fileSize: number; }
인터페이스 FileData를 정의합니다.
type MakeReadonly<T, K extends keyof T> = { readonly [P in keyof T]: P extends K ? T[P] : T[P]; };
다음으로, MakeReadonly라는 타입을 정의합니다. 이 타입은 두 개의 제네릭 타입 매개변수를 가지며, T는 프로퍼티를 수정할 대상 인터페이스(FileData)이고, K는 읽기 전용으로 만들고자 하는 프로퍼티 키들의 유니온 타입(createDate, fileSize)입니다. MakeReadonly 타입은 T에 지정된 인터페이스의 모든 프로퍼티를 순회하면서, 각 프로퍼티가 K에 포함되어 있으면 그 프로퍼티를 읽기 전용으로 만듭니다. 그렇지 않으면 그대로 둡니다.
type ReadonlyFileData = MakeReadonly<FileData, 'createDate' | 'fileSize'>;
MakeReadonly를 사용하여 ReadonlyFileData라는 새로운 타입을 생성합니다. 이 타입은 FileData의 프로퍼티를 기반으로 하여 구조는 같되, createDatefileSize 프로퍼티를 읽기 전용으로 만듭니다.
interface FileData { fileName: string; createDate: string; fileSize: number; } // Mapped Type type MakeReadonly<T, K extends keyof T> = { readonly [P in keyof T]: P extends K ? T[P] : T[P]; }; type ReadonlyFileData = MakeReadonly<FileData, 'createDate' | 'fileSize'>; const fileInfo: ReadonlyFileData = { fileName: 'example.txt', createDate: '2024-03-01', fileSize: 1024, }; fileInfo.createDate = '2024-03-03'; // Error: 읽기 전용 프로퍼티 fileInfo.fileSize = 2048; // Error: 읽기 전용 프로퍼티
이렇게 함으로써 ReadonlyFileData 타입을 사용하면 해당 프로퍼티들을 읽기 전용으로만 사용할 수 있게 됩니다. 이것은 11장에서 소개할 유틸리티 타입 중 하나인 Readonly를 정의하는 것과 유사한 동작입니다.
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
Readonly는 주어진 타입 T의 모든 혹은 조건부로 프로퍼티를 읽기 전용으로 만들어주는 역할을 합니다. 이 점에서 MakeReadonly와 유사함을 알 수 있습니다.

2) 유저 정보 수정 API

다음과 같은 유저 정보가 있을 때, 유저 정보를 조회하는 함수는 유저 정보를 반환할 것입니다.
interface UserData { id: number; name: string; email: string; } // UserData 가져오기 function fetchUserData(): UserData { // ... }
만약 이 유저 정보를 수정한다고 하면 API는 다음과 같은 형태일 것입니다.
interface NewUserData { id?: number; name?: string; email?: string; } function updateUserData(newData: NewUserData) { // ... }
여기서 인터페이스 UserDataNewUserData는 유사한 구조인데 이러한 중복은 지양해야 합니다.
interface UserData { id: number; name: string; email: string; } interface NewUserData { id?: number; name?: string; email?: string; }
위의 인터페이스에서 반복되는 구조는 다음과 같은 방식으로 재활용이 가능합니다.
type UserDataUpdate = { id?: UserData['id']; name?: UserData['name']; email?: UserData['email']; };
이 코드를 좀 더 간결하게 표현한다면 다음과 같습니다.
type UserDataUpdate = { [K in 'id' | 'name' | 'email']?: UserData[K] };
만약 keyof 문법을 적용한다면 더 간결하게 표현이 가능합니다.
type UserDataUpdate = { [K in keyof UserData]?: UserData[K] };
위 코드는 UserData 타입의 각 프로퍼티를 순회하며 선택적(?:)으로 만드는 Mapped Type을 정의합니다. 이를 통해 UserData의 각 프로퍼티를 업데이트할 때 필요한 프로퍼티만 선택적으로 업데이트를 수행할 수 있습니다. 이것은 11장에서 소개할 유틸리티 타입 중 하나인 Partial을 정의하는 것과 유사한 동작입니다.
type Partial<T> = { [P in keyof T]?: T[P]; };
Partial은 주어진 타입 T의 모든 프로퍼티를 선택적으로 만들어주는 역할을 합니다. 이를 통해 특정 타입의 부분 집합을 만족하는 프로퍼티를 업데이트 한다는점에서 UserDataUpdate와 유사함을 알 수 있습니다.
 

9.4 keyof 연산자

keyof 연산자는 객체의 모든 프로퍼티를 문자열 형태의 유니온 타입으로 추출하는 연산자입니다.
interface Employee { name: string; age: number; salary: number; } type ExtractEmployee = keyof Employee;
기본적인 연산자의 사용 방법은 위와 같습니다. keyof 연산자는 객체에만 사용 가능하며 타입에만 사용이 가능합니다. ExtractEmployee타입에는 유니온 타입으로 “name” | “age” | “salary” 타입이 지정됩니다.
interface Student { name: string; age: number; school: string; major: string; graduation: boolean; } function extractKeyValue(student: Employee, key: string) { return student[key]; } const student: Student = { name: "김철수", age: 30, school: "서울대학교", major: "컴퓨터공학과", graduation: true }; extractKeyValue(student, "name");
keyof 연산자를 사용하는 예시 중 하나는 함수에서 Student의 프로퍼티를 추출해내서 타입으로 사용하는 방법입니다. 위의 코드의 경우 key의 타입을 string으로 받으면 함수의 return에서 오류가 발생합니다. 왜냐하면 모든 문자열의 값이 student 객체의 key라고 볼 순 없기 때문입니다. 함수에 인자를 name이라고 넘겨주면 student 객체의 프로퍼티가 될 순 있지만 name2라고 넘겨줄 경우 student 객체의 프로퍼티가 아닙니다.
function extractKeyValue( person: Student, key: "name" | "age" | "school" | "major" | "graduation" ) { return student[key]; } extractKeyValue(student, "name");
함수의 매개변수로 key의 타입을 유니온 타입으로 지정을 해줄 수도 있습니다. 이 경우 함수의 return에서 오류가 발생하진 않습니다. 하지만 객체의 프로퍼티가 많을수록 그만큼 유니온 타입을 지정해 주어야 하고 이로 인해 보일러 플레이트가 발생하고 비효율적인 코드를 작성하게 됩니다.
function extractKeyValue(person: Student, key: keyof Student) { return student[key]; }
위의 두가지 케이스의 문제점을 해결할 수 있는 것이 keyof 연산자입니다. 객체의 프로퍼티가 아무리 많아도, 프로퍼티가 추가 및 제거되거나 이름이 바뀌어도 매개변수에서 일일이 타입을 수정해 줄 필요가 없이 아주 쉽게 객체의 프로퍼티들을 추출해내서 사용할 수 있습니다.