TypeScript 유틸리티 타입
유틸리티 타입이란?
- 기존 타입을 기반으로 새로운 타입을 생성 혹은 변환
기본 유틸리티 타입
Partial
- 모든 속성을 **옵셔널 속성(?)**으로 변경
// interface
interface Info {
name: string;
age: number;
}
interface MyInfo extends Partial<Info> {}
// type
type Info = {
name: string;
age: number;
};
type MyInfo = Partial<Info>;
위의 코드에서 MyInfo 타입 위에 마우스를 올리면 출력된 결과이다
Partial을 적용하여 기존 타입의 모든 속성을 **옵셔널 속성(?)**으로 변경한 것을 확인할 수 있다
즉, Partial은 기존 타입의 속성을 기반으로 옵셔널 속성으로 변경하는 유틸리티 타입 중 하나
Required
- 타입의 모든 옵셔널 속성을 필수 속성으로 변경
- Partial의 반대 기능
// interface
interface Info {
name?: string;
age?: number;
}
interface MyInfo extends Required<Info> {}
// type
type Info = {
name?: string;
age?: number;
};
type MyInfo = Required<Info>;
Required를 적용하여 기존 타입의 모든 옵셔널 속성(?)이 필수 속성으로 변경된 것을 확인
Partial과 반대의 기능으로 동작
Readonly
- 모든 속성을 읽기 전용(
readonly)로 변경
// interface
interface Info {
name?: string;
age?: number;
}
interface MyInfo extends Readonly<Info> {}
// type
type Info = {
name?: string;
age?: number;
};
type MyInfo = Readonly<Info>;
Readonly 유틸리티 타입을 이용하여 Info의 모든 속성에 대해 readonly를 적용
Pick
- 특정 속성만 선택하여 새로운 타입을 생성
// interface
interface Info {
name?: string;
age?: number;
iq: number;
}
interface MyInfo extends Pick<Info, "name"> {}
// type
type Info = {
name?: string;
age?: number;
iq: number;
};
type MyInfo = Pick<Info, "name">;
Pick 유틸리티 타입을 사용할 경우 Pick<타입, 타입내 속성(들)> 형식으로 사용하면 name속성만 가진 새로운 타입이 생성된다.
type MyInfo = Pick<Info, "name" | "iq">;
여러 속성을 가져와야 할 경우, |을 사용하면 된다.
그럼 MyInfo의 속성은 name과 iq를 가질 수 있다.
Omit
- 특정 속성을 제외하여 새로운 타입을 생성
// interface
interface Info {
name?: string;
age?: number;
iq: number;
}
interface MyInfo extends Omit<Info, "name"> {}
// type
type Info = {
name?: string;
age?: number;
iq: number;
};
type MyInfo = Omit<Info, "name">;
Omit 유틸리티 타입을 사용할 경우 Omit<타입, 제거할 속성(들)> 형식으로 사용하면 name속성이 제거된 새로운 타입이 생성된다.
type MyInfo = Omit<Info, "name" | "iq">;
Omit 또한 여러 속성들을 제거할 경우, |을 이용하면 된다.
그럼 MyInfo에 age속성만 존재하게 된다.
Record
- 키와 값을 특정 타입으로 매핑
// type
type MyInfo = Record<"name" | "age", string | number>;
Record 유틸리티 타입을 사용할 경우 Record<생성할 속성, 참고할 타입> 형식으로 사용하면,
name, age 속성에 string 혹은 number의 타입을 가지고 생성된다.
// interface
interface Info {
name: string;
age: number;
}
interface MyInfo extends Record<"hello" | "world", Info> {}
// type
type Info = {
name: string;
age: number;
};
type MyInfo = Record<"hello" | "world", Info>;
위와 같이 기존 타입을 이용하여 이용하여 새롭게 매핑이 가능하다.
이미지를 보면 MyInfo는 hello, world의 key를 가지며, 각 key들은 Info 타입 객체와 매핑된 것을 확인할 수 있다.
Record 사용 시, 유의 사항
-
Record는 TypeScript의 유틸리티 타입으로, 특정 키와 값을 제네릭 기반으로 동적으로 매핑한다.
interface는 Record를 **확장(extends)**하여 사용할 수는 있지만, Record 자체를 직접 생성하는 문법으로 사용할 수 없다. -
Record는 객체 타입을 생성하며, 키(key)는 string | number | symbol 타입만 가능
다른 키 타입을 사용하려고 하면 TypeScript에서 오류가 발생
조건부 유틸리티 타입
Exclude
- 유니언 타입에서 특정 타입을 제외한 새로운 타입 을 생성할 때 사용
type Status = "success" | "error" | "loading";
type One = Exclude<Status, "success">;
Exclude는 Exclude<유니언 타입, 제거하고 싶은 타입>을 작성하면 제거하고 싶은 타입을 제외한 남은 타입을 생성한다.
여러 타입을 제거하고 싶다면 |연산자를 이용하여 제거하면 된다.
Omit과 똑같은 목적으로 사용하지만, 차이점이 존재한다.
Omit: 객체 타입에서 특정 속성을 제거하여 새로운 객체 타입을 생성.Exclude: 유니언 타입에서 특정 타입을 제외하여 새로운 유니언 타입을 생성.
즉, Exclude는 interface를 직접적으로 사용할 수 없다.
Extract
- 유니언 타입에서 특정 타입만 추출하여 새로운 타입을 생성
- Exclude와 반대
type Status = "success" | "error" | "loading";
type One = Extract<Status, "success">;
Extract는 Extract<유니언 타입, 추출 타입>을 작성하면 추출 타입에 작성한 타입만 생성된다.
여러 타입을 추출하고 싶다면 |연산자를 이용하여 된다.
Extract 또한 Exclude와 같은 유니온 타입에서만 조작이 가능하다.
만약, 특정 타입을 추출해야하는 대상이 객체 타입일 경우 Pick을 이용하면 된다.
NonNullable
null과undefined를 제외한 새로운 타입을 생성하는데 사용
type Status = "success" | "error" | "loading" | null | undefined;
type One = NonNullable<Status>;
NonNullable은 NonNullable<null,undefined를 제거할 타입>을 작성하면
null, undefined가 제거된 새로운 타입이 생성된다.
type Info = {
name?: string | undefined;
age: null | number;
};
type MappingInfo = {
[k in keyof Info]: NonNullable<Info[k]>;
};
객체 속성내의 모든 속성에 대해 null, undefined를 제거하려면 매핑된 타입을 사용하면 된다.
하지만, name속성을 보면 undefined가 제거되지 않은 상태로 MappintInfo타입이 생성된다.
왜 name 속성에 undefined가 남아있는 것처럼 보이는가?
-
NonNullable은 타입에서 null과 undefined를 제거하여 string타입만 남는다.
-
?(옵셔널 속성)는 속성의 존재 여부를 나타내는 메타 정보로 남아있다.
-
옵셔널 속성(name?)은 내부적으로 컴파일러가 name: string | undefined로 처리
-
결론: undefined가 제거된 후에도 옵셔널 속성의 의미를 유지하기 위해 undefined가 포함된 것처럼 보인다.
만약 옵셔널 속성도 함께 제거를 하고 싶다면
type Info = {
name?: string | undefined;
age: null | number;
};
type MappingInfo = {
[k in keyof Info]-?: NonNullable<Info[k]>;
};
위와 같이 -?를 이용하면 옵셔널 속성이 제거가 된다.
함수 관련 유틸리티 타입
ReturnType
- 함수 타입의 반환 타입을 추출하는데 사용
type F = () => string;
type FType = ReturnType<F>;
ReturnType은 ReturnType<함수의 반환 타입을 추출할 타입>를 작성하면
함수의 반환값의 타입이 생성된다.
function Calculate(a: number, b: number): number | void {
return a + b;
}
type CaculateType = ReturnType<typeof Caculate>;
typeof와 ReturnType을 조합하면, 함수 선언으로부터 반환 타입을 추출할 수 있다.
이 방법은 JavaScript 코드에서 실제 함수의 반환 타입을 TypeScript로 확인하는 데 유용하다.
위의 이미지와 같이 Caculate함수의 반환 타입을 확인할 수 있다.
Parameters
- 함수 타입의 매개변수 타입을 추출하는 데 사용
type F = (x: number, y: string) => string;
type FType = Parameters<F>;
Parameters<parameter 타입을 추출할 함수 타입>와 같은 형태를 작성하게 되면,
이미지와 같이 튜플 형태로 함수의 parameter 타입들을 반환한다.
function F(): void {
console.log("Hello World");
}
type FType = Parameters<typeof F>;
위의 코드에서 함수의 parameter가 존재하지 않을 경우, 빈 튜플([])을 반환한다.
즉, 매개변수가 존재하지 않는 것을 명확히 표현
function F1(x: number, y: string): void {
console.log("Hello World");
}
type FType = Parameters<typeof F1>;
function F2(args: FType): void {
console.log(`${args[0]} ${args[1]}`);
}
F2(["hello", "world"]); // type Error
F2([1, "hello world"]); // 1 hello world
위의 경우, F1의 매개변수 타입을 FType으로 추출하여 F2 함수의 매개변수 타입으로 지정하였다.
FType은 튜플 형태의 타입으로, 매개변수의 순서와 타입이 고정되어 있다.
따라서, F2 함수에 값을 전달할 때는 튜플의 각 위치에 맞는 타입을 정확히 입력해야 정상적으로 동작한다.
ConstructorParameters
- 클래스 생성자의 매개변수 타입을 추출하여 튜플 형태로 반환하는 데 사용
class Info {
constructor(name: string, age: number) {}
}
type MyInfo = ConstructorParameters<typeof Info>;
ConstructorParameters<생성자 타입>와 같이 작성하면,
클랙스 생성자의 매개변수 타입을 추출하여 튜플 형태로 반환한다.
class Info {
constructor() {}
}
type MyInfo = ConstructorParameters<typeof Info>;
위와 같이 클래스의 생성자 parameter가 존재하지 않을 경우, 빈 튜플([])을 반환
InstanceType
- 클래스 생성자로부터 해당 클래스의 인스턴스 타입을 추출하는데 사용
class Info {
name: string;
age: number;
constructor(name2: string, age2: number) {
this.name = name2;
this.age = age2;
}
}
type MyInfo = InstanceType<typeof Info>;
InstanceType<클래스>를 작성하면, 해당 클래스의 인스턴스 타입을 추출할 수 있다.
위 코드에서 typeof Info는 Info 클래스의 생성자 타입을 나타내며, InstanceType은 생성자의 반환 타입을 추출하며, 클래스이 인스턴스 타입을 반환
type MyInfo = {
name: string;
age: number;
};
따라서, MyInfo는 Info 클래스의 인스턴스 타입과 동일하며, 위와 같은 결과를 가진다.
기타 유틸리티 타입
ThisType
- 특수한 유틸리티 타입
- 객체 컨텍스트에서 this의 타입을 설정하는 데 사용
- 주로 객체 리터럴의 컨텍스트에서 동작
interface Info {
name: string;
greeting(): void;
}
const MyInfo: Info & ThisType<Info> = {
name: "JJamVa",
greeting() {
console.log(this.name);
},
};
MyInfo.greeting(); // JJamVa
위의 코드는 MyInfo의 타입을 Info로 지정함과 동시에, MyInfo 객체의 메서드 내부에서 사용되는 this의 타입을 Info로 설정한 코드이다.
ThisType<Info>를 통해 해당 객체 리터럴 내에서 this는 Info 타입으로 제한된다.
따라서, MyInfo 안에서 사용되는 this는 name: string과 greeting(): void타입을 가지는 객체로 명시된다.
ThisType의 장단점
-
장점
- TypeScript가 객체 리터럴에서 this 타입을 추론하지 못하는 문제 해결
- this가 명확한 타입 지정 가능
- 잘못된 this 참조 ex) window 객체
-
단점
- 객체 리터럴에서만 사용 가능
- 런타임에 영향을 미치지 않음(오로지 컴파일 단계에서 타입 체크만을 위해 사용)
Awaited
- Promise나 비동기 함수에서 반환된 값의 타입을 추출하는 데 사용
async function fetchData(): Promise<string> {
return "data";
}
type DataType = Awaited<ReturnType<typeof fetchData>>;
fetchData 함수는 비동기 함수이며, Promise<string> 타입의 값을 반환한다.
DataType은 ReturnType을 사용하여 fetchData 함수의 반환 타입인 Promise<string>를 가져온다.
이때, Promise<string> 내부의 값을 추출하기 위해 Awaited를 사용한다.
Awaited<Promise<string>>은 Promise를 풀어 최종적으로 string 타입을 반환한다.
async function fetchData(): Promise<Promise<string>> {
return "data";
}
type DataType = Awaited<ReturnType<typeof fetchData>>; // string
위의 코드에서 fetchData의 반환 타입이 Promise<Promise<string>>으로 2중첩의 타입이 되어있는 경우도 있다.
이럴 경우 Awaited는 중첩된 Promise 구조를 재귀적으로 해제하여 최하위의 타입을 반환