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
구조를 재귀적으로 해제하여 최하위의 타입을 반환