Skip to main content

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>;
note

image

위의 코드에서 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>;
note

image

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>;
note

image

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">;
note

image

Pick 유틸리티 타입을 사용할 경우 Pick<타입, 타입내 속성(들)> 형식으로 사용하면 name속성만 가진 새로운 타입이 생성된다.

tip
type MyInfo = Pick<Info, "name" | "iq">;

여러 속성을 가져와야 할 경우, |을 사용하면 된다.
그럼 MyInfo의 속성은 nameiq를 가질 수 있다.

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">;
note

image

Omit 유틸리티 타입을 사용할 경우 Omit<타입, 제거할 속성(들)> 형식으로 사용하면 name속성이 제거된 새로운 타입이 생성된다.

type MyInfo = Omit<Info, "name" | "iq">;

Omit 또한 여러 속성들을 제거할 경우, |을 이용하면 된다.
그럼 MyInfo에 age속성만 존재하게 된다.

Record

  • 키와 값을 특정 타입으로 매핑
// type
type MyInfo = Record<"name" | "age", string | number>;
note

image

Record 유틸리티 타입을 사용할 경우 Record<생성할 속성, 참고할 타입> 형식으로 사용하면,
name, age 속성에 string 혹은 number의 타입을 가지고 생성된다.

info
// 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>;

image

위와 같이 기존 타입을 이용하여 이용하여 새롭게 매핑이 가능하다.
이미지를 보면 MyInfo는 hello, world의 key를 가지며, 각 key들은 Info 타입 객체와 매핑된 것을 확인할 수 있다.

danger

Record 사용 시, 유의 사항

  • Record는 TypeScript의 유틸리티 타입으로, 특정 키와 값을 제네릭 기반으로 동적으로 매핑한다.
    interface는 Record를 **확장(extends)**하여 사용할 수는 있지만, Record 자체를 직접 생성하는 문법으로 사용할 수 없다.

  • Record는 객체 타입을 생성하며, 키(key)는 string | number | symbol 타입만 가능
    다른 키 타입을 사용하려고 하면 TypeScript에서 오류가 발생


조건부 유틸리티 타입

Exclude

  • 유니언 타입에서 특정 타입을 제외한 새로운 타입을 생성할 때 사용
type Status = "success" | "error" | "loading";

type One = Exclude<Status, "success">;
note

image

Exclude는 Exclude<유니언 타입, 제거하고 싶은 타입>을 작성하면 제거하고 싶은 타입을 제외한 남은 타입을 생성한다.
여러 타입을 제거하고 싶다면 |연산자를 이용하여 제거하면 된다.

Omit과 똑같은 목적으로 사용하지만, 차이점이 존재한다.

  • Omit: 객체 타입에서 특정 속성을 제거하여 새로운 객체 타입을 생성.
  • Exclude: 유니언 타입에서 특정 타입을 제외하여 새로운 유니언 타입을 생성.

즉, Exclude는 interface를 직접적으로 사용할 수 없다.

Extract

  • 유니언 타입에서 특정 타입만 추출하여 새로운 타입을 생성
  • Exclude와 반대
type Status = "success" | "error" | "loading";

type One = Extract<Status, "success">;
note

image

Extract는 Extract<유니언 타입, 추출 타입>을 작성하면 추출 타입에 작성한 타입만 생성된다.
여러 타입을 추출하고 싶다면 |연산자를 이용하여 된다.

Extract 또한 Exclude와 같은 유니온 타입에서만 조작이 가능하다.
만약, 특정 타입을 추출해야하는 대상이 객체 타입일 경우 Pick을 이용하면 된다.

NonNullable

  • nullundefined를 제외한 새로운 타입을 생성하는데 사용
type Status = "success" | "error" | "loading" | null | undefined;

type One = NonNullable<Status>;
note

image

NonNullable은 NonNullable<null,undefined를 제거할 타입>을 작성하면 null, undefined가 제거된 새로운 타입이 생성된다.

tip
type Info = {
name?: string | undefined;
age: null | number;
};

type MappingInfo = {
[k in keyof Info]: NonNullable<Info[k]>;
};

image

객체 속성내의 모든 속성에 대해 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]>;
};

image

위와 같이 -?를 이용하면 옵셔널 속성이 제거가 된다.

함수 관련 유틸리티 타입

ReturnType

  • 함수 타입의 반환 타입을 추출하는데 사용
type F = () => string;

type FType = ReturnType<F>;
note

image

ReturnType은 ReturnType<함수의 반환 타입을 추출할 타입>를 작성하면 함수의 반환값의 타입이 생성된다.

tip
function Calculate(a: number, b: number): number | void {
return a + b;
}

type CaculateType = ReturnType<typeof Caculate>;

image

typeofReturnType을 조합하면, 함수 선언으로부터 반환 타입을 추출할 수 있다.
이 방법은 JavaScript 코드에서 실제 함수의 반환 타입을 TypeScript로 확인하는 데 유용하다.
위의 이미지와 같이 Caculate함수의 반환 타입을 확인할 수 있다.

Parameters

  • 함수 타입의 매개변수 타입을 추출하는 데 사용
type F = (x: number, y: string) => string;

type FType = Parameters<F>;
note

image

Parameters<parameter 타입을 추출할 함수 타입>와 같은 형태를 작성하게 되면,
이미지와 같이 튜플 형태로 함수의 parameter 타입들을 반환한다.

tip
function F(): void {
console.log("Hello World");
}

type FType = Parameters<typeof F>;

image

위의 코드에서 함수의 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>;
note

image

ConstructorParameters<생성자 타입>와 같이 작성하면,
클랙스 생성자의 매개변수 타입을 추출하여 튜플 형태로 반환한다.

class Info {
constructor() {}
}

type MyInfo = ConstructorParameters<typeof Info>;

image

위와 같이 클래스의 생성자 parameter가 존재하지 않을 경우, 빈 튜플([])을 반환

InstanceType

  • 클래스 생성자로부터 해당 클래스의 인스턴스 타입을 추출하는데 사용
class Info {
name: string;
age: number;
constructor(name2: string, age2: number) {
this.name = name2;
this.age = age2;
}
}

type MyInfo = InstanceType<typeof Info>;
note

image

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
note

위의 코드는 MyInfo의 타입을 Info로 지정함과 동시에, MyInfo 객체의 메서드 내부에서 사용되는 this의 타입을 Info로 설정한 코드이다.
ThisType<Info>를 통해 해당 객체 리터럴 내에서 this는 Info 타입으로 제한된다.
따라서, MyInfo 안에서 사용되는 this는 name: stringgreeting(): void타입을 가지는 객체로 명시된다.

tip

ThisType의 장단점

  • 장점

    • TypeScript가 객체 리터럴에서 this 타입을 추론하지 못하는 문제 해결
    • this가 명확한 타입 지정 가능
    • 잘못된 this 참조 ex) window 객체
  • 단점

    • 객체 리터럴에서만 사용 가능
    • 런타임에 영향을 미치지 않음(오로지 컴파일 단계에서 타입 체크만을 위해 사용)

Awaited

  • Promise나 비동기 함수에서 반환된 값의 타입을 추출하는 데 사용
async function fetchData(): Promise<string> {
return "data";
}

type DataType = Awaited<ReturnType<typeof fetchData>>;
note

fetchData 함수는 비동기 함수이며, Promise<string> 타입의 값을 반환한다.
DataType은 ReturnType을 사용하여 fetchData 함수의 반환 타입인 Promise<string>를 가져온다.
이때, Promise<string> 내부의 값을 추출하기 위해 Awaited를 사용한다.
Awaited<Promise<string>>은 Promise를 풀어 최종적으로 string 타입을 반환한다.

tip
async function fetchData(): Promise<Promise<string>> {
return "data";
}

type DataType = Awaited<ReturnType<typeof fetchData>>; // string

위의 코드에서 fetchData의 반환 타입이 Promise<Promise<string>>으로 2중첩의 타입이 되어있는 경우도 있다.
이럴 경우 Awaited는 중첩된 Promise 구조를 재귀적으로 해제하여 최하위의 타입을 반환