Documentation - Creating Types from Types (opens in a new tab)
원시 타입에서 벗어나서, 특정 타입을 이용해 필요한, 좀더 복잡한 타입을 만들어내는 방법들을 알아봅시다!
특히 객체 타입을 가장 많이 다룰 예정입니다.
Indexed Access Type
인덱스 접근 타입은 따로 배우지 않아도 JS를 알고 있다면 저절로 알게되는 방식이라고 할 수 있다.
JS에서 object의 property에 접근하기 위해 대괄호 표기법([])사용하듯, 대괄호를 사용하여 특정 객체 타입의 속성의 타입을 쉽게 추출해준다.
type Person = {
name: string;
age: number;
location: string;
};
type Age = Person['age']; // 'age' 속성의 타입인 'number'를 추출위의 코드에서, **Person['age']**는 Person 타입의 'age' 속성의 타입을 나타냄.
→ 따라서 Age 타입은 number
객체의 모든 속성 타입 추출하기
type PersonKeys = keyof Person; // 'name' | 'age' | 'location'
type PersonTypes = Person[PersonKeys]; // string | number- **
keyof Person**은Person타입의 모든 키를 문자열 리터럴 유니온 타입으로 생성 - 이를 인덱스 접근 타입에 사용하여 **
Person**의 모든 속성의 타입을 유니온 타입으로 생성 - 따라서
PersonTypes타입은 **string | number**가 됨.
indexing에서 사용할 수 있는 것들
indexing type은 그 자체로 타입이기 때문에, union, keyof 등을 모두 사용할 수 있다.
// Union
type I1 = Person["age" | "name"];
type I1 = string | number
// keyof
type I2 = Person[keyof Person];
type I2 = string | number | boolean
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName];
type I3 = string | booleanTry
만약 인덱싱으로 접근하려는 타입이 존재하지 않으면, 에러가 발생한다.
type I1 = Person["alve"];
Property 'alve' does not exist on type 'Person'.Property 'alve' does not exist on type 'Person'.Try배열 타입의 개별 요소 타입가져오기
배열 타입의 경우 [number] 로 개별 요소의 타입에 접근 가능하다.
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
type Person = typeof MyArray[number];
type Person = {
name: string;
age: number;
}
type Age = typeof MyArray[number]["age"];
type Age = number// Or
type Age2 = Person["age"];type Age2 = numberTry
값이 아닌 type!
참고로, 인덱싱에 사용되는 키는 “값”이 아닌 type이어야한다. 즉, 변수를 선언하는 let, const, var로 선언된 string으로는 인덱싱이 불가능하다.
// X
const key = "age";
type Age = Person[key];
Type 'key' cannot be used as an index type.
'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?Type 'key' cannot be used as an index type.
'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?Try// O
type key = "age";
type Age = Person[key];The keyof Type Operator
객체의 Key를 literal type의 union으로 가져온다.
keyof 연산자는 object 타입으로부터 object key의 string 혹은 numeric 리터럴 유니온타입을 가져온다.
예를 들어, 아래의 P 타입은 type P = "x" | "y"와 같다.
type Point = { x: number; y: number };
type P = keyof Point;
만약 object 타입이 index signature를 사용하는 경우, keyof 연산 결과는 index signature의 key타입을 반환한다.
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
// type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
//type M = string | numberTypeScript에서 숫자 인덱스 시그니처를 사용하는 경우({[k: number]: unknown}),
keyof연산의 결과는 **number**로만 제한된다.- TypeScript가 숫자 인덱스 시그니처와 문자열 인덱스 시그니처를 분리하여 다루기 때문.
- 참고로, JavaScript의 배열은 기본적으로 숫자 인덱스를 사용하므로,
TypeScript의 배열 타입(
Array<T>)은 숫자 인덱스 시그니처를 가지고, 그래서 배열 타입의keyof연산의 결과는 **number**로 제한된다.
**{[k: string]: boolean}**와 같이 문자열 인덱스 시그니처를 사용하는 경우,
keyof연산의 결과는 **string | number**가 된다.- JavaScript에서 숫자 인덱스는 내부적으로 문자열로 변환되어 처리되기 때문
(
obj[0]과obj[”0”]은 같음)
The typeof type operator
Javascript는 이미 typeof operator를 식에서 사용할 수 있다.
// Prints "string"
console.log(typeof "Hello world");TypeScript는 type context에서 사용할 수 있는 typeof operator를 제공하여 특정 변수나 프로 퍼티의 타입을 참조할 수 있다.
let s = "hello";
let n: typeof s;
// let n: string기본 타입에서는 딱히 쓸모가 없을 수 있지만, 다른 type 연산자와 함께 쓰였을 때 매우 편하게 타입을 추출해낼 수 있다. 예를 들어, Typescript에서 제공하는 ReturnType<T> 은 함수의 타입을 받아서 해당 함수가 리턴하는 value의 타입을 알려준다.
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>;
// type K = boolean만약, ReturnType<> 제네릭 파라미터에 함수의 타입이아닌 함수를 넣는다면? 당연 에러가 발생.
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<**f**>; // type이 아닌 함수를 넘김
**'f' refers to a value, but is being used as a type here. Did you mean 'typeof f'?'f' refers to a value, but is being used as a type here. Did you mean 'typeof f'?**이때 typeof f 를 넘기면 복잡하게 타이핑을 직접 할 필요 없이 해당 함수의 타입을 넘겨줄 수 있게된다.
이처럼 typeof 연산자는 값 레벨의 변수(식별자)를 통해서 가지고 해당 값의 타입을 쉽게 가져올 수 있다.
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
type P = {
x: number;
y: number;
}Limitations
typeof 연산자는 값을 받아 해당 값의 타입을 반환하는 것이 아니라, 값 대신 변수명이나 변수의 프로퍼티 등과 같은 식별자에 사용이 가능하다. 그래서 아래와 같은 사용은 유효하지 않다.
// Meant to use = ReturnType<typeof msgbox>
let shouldContinue: typeof msgbox("Are you sure you want to continue?");
',' expected.Index Signature and Mapped Type
index signature, mapped type, Record utility 모두 객체 타이핑을 할 때 자주 사용된다.
Index Signature
Index Signature는 객체의 속성 이름을 미리 알 수 없을 때 주로 사용한다.
이를 통해 동적인 key property에 대한 타입을 제공할 수 있다. 예를 들어, 키가 문자열이고 값이 숫자인 객체를 나타내려면 다음과 같이 작성할 수 있다.
type StringToNumberDictionary = {
[key: string]: number;
};Mapped Type
한편, Mapped Type은 기존 타입에서 새로운 타입을 생성하기 위해 사용되고, 이를 통해 기존 타입의 모든 속성에 대해 반복적인 연산을 수행함으로써 반복적 타이핑을 하는 수고를 덜 수 있다.
index signature와 비슷하게 대괄호와 함께 사용하지만 in 키워드 뒤에 유니온 타입이 온다는 점이 다르다.
예를 들어, 모든 속성을 선택적으로 만드는 새로운 타입을 생성해주는 Partial<T> 는 다음과 같이 작성할 수 있다.
type Partial<T> = {
[P in 'daeun' | 'kiwon']?: T[P];
};
{
daeun? string;
kiwon?: string;
}mapped type은 주로 keyof 연산자와 함께 사용될 수 있는데,
keyof 연산자가 유니온타입으로 프로퍼티 키를 가져오기 때문이다.
Mapping Modifiers
타입스크립트의 매핑 타입에서는 매핑 수식어(Mapping Modifiers)라는 것을 사용할 수 있다. 이를 통해 매핑된 타입의 속성을 더 세밀하게 제어할 수 있다.
⇒ readonly, optional 두 가지가 있.
-
readonly: 이 수식어는 매핑된 타입의 속성을 읽기 전용으로 만든다.typescriptCopy code type MyObject = { prop1: number; prop2: string; }; type ReadonlyMyObject = { readonly [K in keyof MyObject]: MyObject[K]; };여기서
ReadonlyMyObject타입은 **MyObject**의 모든 속성이 읽기 전용이 된 새로운 타입. -
optional: 이 수식어는 매핑된 타입의 속성을 선택적으로 만듭니다. 즉, 해당 속성이 없어도 되게끔 만듭니다. 예를 들어,type MyObject = { prop1: number; prop2: string; }; type OptionalMyObject = { optional [K in keyof MyObject]: MyObject[K]; };여기서
OptionalMyObject타입은 **MyObject**의 모든 속성이 선택적인 새로운 타입입니다.
이런 식으로 매핑 수식어를 사용하면 타입의 속성에 대해 더욱 유연한 제어를 할 수 있습니다. 기존의 타입을 바꾸지 않고 새로운 타입을 생성하는 데 유용합니다.
mapping modifier 수식어
수식어(prefix)를 사용한 매핑된 타입은 TypeScript 2.8 버전부터 사용 가능.
prefx - or +. 를 사용해서 modifier를 수정할 수 잇다. (default: +)
type MyObject = {
-readonly [K in keyof SomeType]: SomeType[K]; // readonly 제거
+optional [K in keyof SomeType]?: SomeType[K]; // optional 추가
};// Removes 'optional' attributes from a type's properties
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete<MaybeUser>;
type User = {
id: string;
name: string;
age: number;
}
Key Remapping via as
TypeScript 4.1 부터, as 연산자를 통해 키를 re-map할 수 있다.
as 연산자는 TypeScript에서 주로 두 가지 목적으로 사용된다.
- 타입 단언(Type Assertion): 특정 값이 특정 타입임을 명시적으로 지정할 때.
JavaScript에서 값의 타입을 동적으로 바꾸는 것처럼, TypeScript에서 as 키워드를 사용하면 컴파일러에게 특정 값이 특정 타입이라는 것을 알려줄 수 있다.
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;- Key Remapping in Mapped Types:
TypeScript 4.1 버전부터는 매핑된 타입에서
as연산자를 사용하여 키를 다시 매핑(remap)할 수 있게 되어, 기존 타입의 키를 새로운 타입의 키로 변환할 수 있게 해준다.
type MappedTypeWithNewProperties<T> = {
[K in keyof T as NewKeyType]: T[K]
};위 예제에서 **NewKeyType**는 원하는 새로운 키의 타입이며
, **T[K]**는 해당 새로운 키에 할당될 원본 타입의 해당 속성의 타입.
template literal type과 함께 사용하기.
타입스크립트 4.1이상에서, 매핑된 타입과 템플릿 리터럴 타입을 함께 사용하여 기존 타입을 기반으로 새로운 타입을 만들 수 있다.
예를 들어, 다음과 같은 인터페이스가 있다.
type CityInfo = {
population: number;
country: string;
};일일이 타이핑해주기엔 뭔가 비효율적인 느낌이 강하게든다.
이 때 Mapped type과 템플릿 리터럴 타입을 사용할 수 있다.
type PrefixCityInfo<Type> = {
[Property in keyof Type as `city${Capitalize<string & Property>}`]: Type[Property];
};위의 예시에서 **PrefixCityInfo**는 매핑된 타입으로,
기존 타입 Type의 각 속성에 대해 새로운 속성을 생성합니다. 이 새로운 속성의 이름은 템플릿 리터럴 타입을 사용하여 'city' 접두사와 원래 속성의 이름을 합친 것.(city${Capitalize<string & Property>}).
이 유틸리티 타입은 문자열 타입에만 작용하므로, **Property**라는 타입(이 경우에는 속성 이름의 타입)을 문자열 타입으로 강제하기 위해 **string & Property**와 같은 교차 타입을 사용합니다.
여기서 **Capitalize**는 TypeScript 4.1에서 도입된 유틸리티 타입으로, 문자열의 첫 글자를 대문자로 바꿈.
type Result = PrefixCityInfo<CityInfo>;
// equivalent to:
// type Result = {
// cityPopulation: number;
// cityCountry: string;
// };따라서, CityInfo 타입을 **PrefixCityInfo**로 변환하면, 모든 속성의 이름이 'city'로 시작하는, 카멜케이스를 만족하는 새로운 타입이 생성된다.
이런 방식으로 매핑된 타입과 템플릿 리터럴 타입을 함께 사용하면, 타입의 속성 이름을 동적으로 조작하거나 변환하는 등의 복잡한 타입 변환이 가능하다.
Record Type
Record Type은 TypeScript에서 제공하는 유틸리티 타입 중 하나로, mapped type을 통해 구현되었다.
type Record<K extends keyof any, T> = {
[P in K]: T;
};Record 타입은 키-값 쌍을 모델링하는 간단하고 명확한 방법.
키로 사용될 타입이며, 값으로 사용될 타입을 지정해주기 때문에, 주로 객체의 모든 속성이 동일한 타입이고, 객체의 키가 미리 정의된 몇 가지 특정 값 중 하나일 때 사용하는 것이 좋.
type PetTypes = 'cat' | 'dog' | 'fish';
type PetAges = Record<PetTypes, number>;
// equivalent to:
// type PetAges = {
// cat: number;
// dog: number;
// fish: number;
// };반면, 매핑된 타입은 타입의 모든 속성에 동일한 타입 변환을 적용하거나, 다양한 키-값 변환을 적용하는 데 더욱 유용하다.
따라서, 타입의 키와 값이 미리 알려져 있고 간단한 경우에는 Record 타입을 사용하는 것이 좋고, 타입의 키와 값에 복잡한 변환이 필요한 경우에는 매핑된 타입을 사용하는 것이 더 좋을 수 있다.