개요
Typescript로 개발을 하면서 때론 매개변수의 유형에 따라 다른 타입의 값을 return하는 함수를 작성하고 싶을 수 있다.
예를 들면 다음과 같다.
declare const record: Record<string, string>;
declare const array: string[];
function getObject(group) {
if (group === undefined) {
return record;
}
return array;
}
const arrayResult = getObject("group");
const recordResult = getObject(undefined);
해당 코드에서 arrayResult 변수의 타입은 string[] 이 되어야 하고 recordResult의 타입은 <string, string>이 되어야 한다.
하지만 getObject 함수는 string[] 타입을 return할수도 있고, Record<string, string> 타입을 return 할수도 있다. 즉 return하는 타입의 범위를 좁히지 못하고 넓은 타입을 가지게 된다.
이러한 기존의 문제점과 Typescript 5.8.0의 해결 방법을 비교해 보려고 한다.
기존의 문제점
다음 예제 코드로 기존의 문제점을 다시한번 알아보자.
/**
* @param prompt 사용자에게 보이는 텍스트
* @param selectionKind 사용자가 선택할 수 있는 개수를 나타내는 유형
* @param items 사용자에게 보이는 각 옵션
**/
export async function showQuickPick(
prompt: string,
selectionKind: SelectionKind,
// selectionKind: S,
items: readonly string[]
): Promise<string | string[]> {
// ...
}
enum SelectionKind {
Single,
Multiple,
}
let shoppingList = await showQuickPick(
"Which fruits do you want to purchase?",
SelectionKind.Multiple,
["apples", "oranges", "bananas", "durian"]
);
console.log(`Alright, going out to buy some ${shoppingList.join(", ")}`);
showQuickPick 함수는 명시적으로 string이나 string[] 타입을 return하도록 선언되어 있다.
shoppingList 변수는 showQuickPick 함수에 사용자가 여러개를 선택했다는 의미의 SelectionKind.Multiple과 사용자가 선택한 apples, oranges, bananas, durian을 string[] 타입으로 보내고 있다.
보기에는 전혀 문제가 되어 보이지 않는다.
하지만 이 코드는 해당 부분에서 에러가 발생하게 된다.
/**
* join 함수에서 에러가 발생한다.
* join 함수는 배열의 요소들을 매개변수로 이어주는 역할을 한다.
* 하지만 명시된 타입은 string | string[]
* 때문에 string을 받게 될지, string[]을 받게 될지 알 수 없다.
* string을 받게 되면 배열이 아니기 때문에 join함수가 작동할 수 없기 때문이다.
* 이는 showQuickPick 함수가 string | string[] 이라는 더 넒은 범위의 타입을 가진다는 것을 의미한다.
*/
console.log(`Alright, going out to buy some ${shoppingList.join(", ")}`);
// ~~~~
// 에러!
// 프로퍼티 'join'은 'string | string[]' 타입에 존재하지 않습니다.
// 프로퍼티 'join'은 'string' 타입에 존재하지 않습니다.
문제는 join함수에서 발생한다.
Typescript는 showQuickPick 함수로 return받은 값이 string인지 string[]인지 알 수 없다.
때문에 에러가 발생한다.
기존의 해결 방법
Typescript 5.8.0 이전까지는 해당 문제를 다음과 같은 방법으로 해결하였다.
type QuickPickReturn<S extends SelectionKind> = S extends SelectionKind.Multiple
? string[]
: string;
enum SelectionKind {
Single,
Multiple,
}
export async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[]
): Promise<QuickPickReturn<S>> {
if (items.length < 1) {
throw new Error("At least one item must be provided.");
}
// 모든 옵션에 대해 버튼 생성
let buttons = items.map((item) => ({
selected: false,
text: item,
}));
// 필요한 경우 첫 번째 요소를 기본값으로 설정
if (selectionKind === SelectionKind.Single) {
buttons[0].selected = true;
}
// 이벤트 핸들링 코드...
// 선택된 옵션들 찾기
const selectedItems = buttons
.filter((button) => button.selected)
.map((button) => button.text);
if (selectionKind === SelectionKind.Single) {
// 선택된 (유일한) 첫 번째 옵션 고르기
return selectedItems[0] as QuickPickReturn<S>;
} else {
// 선택된 모든 옵션들 반환
return selectedItems as QuickPickReturn<S>;
}
}
// `SelectionKind.Multiple`이면 `string[]` - 동작함 ✅
let shoppingList: string[] = await showQuickPick(
"Which fruits do you want to purchase?",
SelectionKind.Multiple,
["apples", "oranges", "bananas", "durian"]
);
// `SelectionKind.Single`이면 `string` - 동작함 ✅
let dinner: string = await showQuickPick(
"What's for dinner tonight?",
SelectionKind.Single,
["sushi", "pasta", "tacos", "ugh I'm too hungry to think, whatever you want"]
);
console.log(`Alright, going out to buy some ${shoppingList.join(", ")}`);
이전까지 Typescript에서 고차 조건부 타입을 반환하는 함수를 구현하려면 다음과 같은 타입 단언이 필요했다.
type QuickPickReturn<S extends SelectionKind> = S extends SelectionKind.Multiple
? string[]
: string;
enum SelectionKind {
Single,
Multiple,
}
if (selectionKind === SelectionKind.Single) {
// 선택된 (유일한) 첫 번째 옵션 고르기
return selectedItems[0] as QuickPickReturn<S>;
} else {
// 선택된 모든 옵션들 반환
return selectedItems as QuickPickReturn<S>;
}
타입 단언은 타입스크립트 컴파일러보다 개발자가 더 타입을 잘 알고 있을때 사용한다.
즉 타입 단언을 사용하는 것은 타입스크립트 컴파일러가 수행하는 적절한 타입 추론을 무시하기 때문에 이상적인 방법이 아니다.
Typescript 5.8.0은 타입 단언을 피하고자 반환문에서 조건부 타입을 제한적으로 검사하는 기능을 추가하였다.
Typescript 5.8.0에서의 해결 방법
type QuickPickReturn<S extends SelectionKind> = S extends SelectionKind.Multiple
? string[]
: S extends SelectionKind.Single
? string
: never;
Typescript 5.8.0에서는 조건부 타입에서 사용된 제네릭 매개변수에 대해 제어 흐름 분석을 수행하고, 각 매개변수의 좁혀진(narrowed) 타입을 사용하여 조건부 타입을 인스턴스화한 후, 이를 새로운 타입과 비교한다.
SelectedKind.Multiple이라면 string[]을, 아니라면 SelectionKind.Single인지 검사한다. 맞다면 string을 아니라면 never타입을 줌으로써 올바르게 타입을 감지한다.
작동방식은 다음 코드에서 확인이 가능하다.
// type QuickPickReturn<S extends SelectionKind> = S extends SelectionKind.Multiple
// ? string[]
// : string;
type QuickPickReturn<S extends SelectionKind> = S extends SelectionKind.Multiple
? string[]
: S extends SelectionKind.Single
? string
: never;
enum SelectionKind {
Single,
Multiple,
}
export async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[]
): Promise<QuickPickReturn<S>> {
if (items.length < 1) {
throw new Error("At least one item must be provided.");
}
// 모든 옵션에 대해 버튼 생성
let buttons = items.map((item) => ({
selected: false,
text: item,
}));
// 필요한 경우 첫 번째 요소를 기본값으로 설정
if (selectionKind === SelectionKind.Single) {
buttons[0].selected = true;
}
// 이벤트 핸들링 코드...
// 선택된 옵션들 찾기
const selectedItems = buttons
.filter((button) => button.selected)
.map((button) => button.text);
if (selectionKind === SelectionKind.Single) {
// 이런! 호출자는 하나의 항목만을 기대하지만 배열을 반환합니다!
// 올바르게 string[]을 반환함을 확인
return selectedItems;
// ~~~~~~
// 에러! 'string[]' 타입을 'string' 타입에 할당할 수 없습니다.
} else {
// 앗! 호출자는 배열을 기대하지만 하나의 항목을 반환합니다!
// 올바르게 string임을 확인
return selectedItems[0];
// ~~~~~~
// error! 'string[]' 타입을 'string' 타입에 할당할 수 없습니다.
}
}
// `SelectionKind.Multiple`이면 `string[]` - 동작함 ✅
let shoppingList: string[] = await showQuickPick(
"Which fruits do you want to purchase?",
SelectionKind.Multiple,
["apples", "oranges", "bananas", "durian"]
);
// `SelectionKind.Single`이면 `string` - 동작함 ✅
let dinner: string = await showQuickPick(
"What's for dinner tonight?",
SelectionKind.Single,
["sushi", "pasta", "tacos", "ugh I'm too hungry to think, whatever you want"]
);
console.log(`Alright, going out to buy some ${shoppingList.join(", ")}`);
해당 코드의 46번, 52번 라인은 다음과 같은 에러를 발생시킨다.
if (selectionKind === SelectionKind.Single) {
// 이런! 호출자는 하나의 항목만을 기대하지만 배열을 반환합니다!
// 올바르게 string[]을 반환함을 확인
return selectedItems;
// ~~~~~~
// 에러! 'string[]' 타입을 'string' 타입에 할당할 수 없습니다.
} else {
// 앗! 호출자는 배열을 기대하지만 하나의 항목을 반환합니다!
// 올바르게 string임을 확인
return selectedItems[0];
// ~~~~~~
// error! 'string[]' 타입을 'string' 타입에 할당할 수 없습니다.
}
해당 에러를 통하여 Typescript 컴파일러가 올바르게 타입을 인식하고 있음을 알 수 있다.
참고문헌
본 글은 다음 내용들을 참고하여 작성되었습니다.
https://velog.io/@typo/announcing-typescript-5-8-beta
[번역] 타입스크립트 5.8 베타를 소개합니다
타입스크립트 5.8 베타 버전이 1월 29일에 릴리즈되었습니다. 이번 버전에서는 특히 조건부 반환 타입을 체크하는 데에 있어 두드러지는 변경점이 있다고 합니다. 한번 확인해보세요!
velog.io
https://github.com/microsoft/TypeScript/pull/56941
Narrow generic conditional and indexed access return types when checking return statements by gabritto · Pull Request #56941 ·
Fixes #33912. Fixes #33014. Motivation Sometimes we want to write functions whose return type is picked between different options, depending on the type of a parameter. For instance: declare const ...
github.com
'기타' 카테고리의 다른 글
Typescript 경로 모듈화, 경로 정규화 과정과 5.8.0 버전에서의 최적화 (2) | 2025.03.03 |
---|---|
Linux 해외 아이피 접속 차단을 위한 ufw 방화벽 설정 방법 (3) | 2025.03.02 |
라즈베리파이 외부 ssh 접속을 위한 포트포워딩 (0) | 2025.02.01 |