5장. 타입 활용하기
5.1 조건부 타입
- TS 의 조건부 타입은 JS 의 삼항 연산자와 동일하게 사용이 가능
- 중복되는 타입 코드를 줄이고 상황에 따른 적절한 타입을 얻거나, 더욱 정확한 타입 추론이 가능하다
5.1.1 extends 와 제네릭을 활용한 조건부 타입
- 조건부 타입으로 extends 가 사용되면 인터페이스의 그것과는 다른 의미로 사용이 된다
// 타입 T 가 U 에 할당이 가능하면 X 타입, 아니면 Y 타입으로 결정
T extends U ? X : Y;
// 사용 예시
interface Bank {
finalcialCode: string;
companyName: string;
name: string;
fullName: string;
}
interface Card {
finalcialCode: string;
companyName: string;
name: string;
appCardType?: string;
}
type PayMethod<T> = T extends 'card' ? Card : Bank;
type CartPayMethodType = PayMethod<'card'>;
type BankPayMethodType = PayMethod<'bank'>;
5.1.2 조건부 타입을 사용하지 않았을 때의 문제점
- 조건부 타입을 사용하지 않은 코드 예시
- 아래의 코드는 결과적으로 useGetRegisteredList 가 PayMethodType[] 타입을 반환하게 되는데, 해당 타입은 결국
type PayMethodType = PayMethodInfo<Card> | PayMethodInfo<Bank>
이므로PayMethodInfo<Card>
와PayMethodInfo<Bank>
의 유니온 타입으로 추론이 되는 구조를 가진다 - 하지만 해당 함수는 매개변수로
'card' | 'appcard' | 'bank'
를 받아 타입을 확정해서 반환하는 것을 의도한 함수이므로 의도와는 다른 결과를 가진다
** [p.154] 아 설명 진짜...... 걍 코드만 줬으면 더 빨리 이해했을거 같습니다 -_-+
type PayMethodType = PayMethodInfo<Card> | PayMethodInfo<Bank>;
export const useGetRegisteredList = (
type: 'card' | 'appcard' | 'bank'
): UseQueryResult<PayMethodType[]> => {
const url = `baeminpay/codes/${type === 'appcard' ? 'card' : type}`;
const fetcher = fetcherFactory<PayMethodType[]>({
onSuccess: (res) => {
const usablePocketList =
res?.filter(
(pocket: PocketInfo<Card> | PocketInfo<Bank>) =>
pocket?.useType === 'USE'
) ?? [];
return usablePocketList;
},
});
const result = useCommonQuery<PayMethodType[]>(url, undefined, fetcher);
return result;
};
5.1.3 extends 조건부 타입을 활용하여 개선하기
- PayMethodType 에 조건부 타입을 적용하여 매개변수를 받는 타이밍에 리턴 데이터의 타입을 확정하여 위에서 문제가 되었던 점을 개선 할 수 있다
- PayMethodType 에 조건부 타입이 적용되어 매개변수로 타입 리터럴이 전달 되는 순간, 제네릭에 의해 useGetRegisteredList 함수의 리턴 데이터의 타입이 확정되어 애초의 의도를 지키는 형태의 함수가 된다
type PayMethodType2<T extends 'card' | 'appcard' | 'bank'> = T extends
| 'card'
| 'appcard'
? Card
: Bank;
// 개선된 useGetRegisteredList
export const useGetRegisteredList = <T extends 'card' | 'appcard' | 'bank'>(
type: T
): UseQueryResult<PayMethodType2<T>[]> => {
const url = `baeminpay/codes/${type === 'appcard' ? 'card' : type}`;
const fetcher = fetcherFactory<PayMethodType2<T>[]>({
onSuccess: (res) => {
const usablePocketList =
res?.filter(
(pocket: PocketInfo<Card> | PocketInfo<Bank>) =>
pocket?.useType === 'USE'
) ?? [];
return usablePocketList;
},
});
const result = useCommonQuery<PayMethodType2<T>[]>(url, undefined, fetcher);
return result;
};
** [p.156]
제네릭과 extends를 함께 사용해 제네릭으로 받는 타입을 제한했다. 따라서 개발자는 잘못된 값을 넘길 수없기 때문에 휴먼 에러를 방지할 수 있다.
** 이 부분 맞나요? extends 를 적용하기 전에도 type: 'card' | 'appcard' | 'bank'
와 같이 매개 변수에 리터럴 유니온 타입을 적용했기 때문에 애초에 잘못된 값을 넘길 여지가 없지 않나요?
** 두번째 내용인 반환 값을 사용자가 원하는 값으로 구체화하는 건 확실한데....
** [p.156] 반환타입 이 그림 의미는 뭔가요? 그리고 어떻게 읽어야 하는거죠?
5.1.4 infer 를 활용해서 타입 추론하기
infer 의 사용
- 조건부 타입의 조건식이 참으로 평가 될 때 infer 키워드 사용이 가능
- 해당 키워드를 사용하는 이유는 추후에 추론된 U 를 제네릭 처럼 활용이 가능하여 다양한 시도가 가능해 지기 때문
T extends number ? X : Y;
// 위의 코드와는 달리 추로 된 U 를 대응되는 식에서 사용이 가능
T extends infer U ? X : Y
- 그런데 위 처럼 사용하면 아래와 같은 의문이 생긴다, 이걸 왜쓰지? 걍 타입 지정하면 되는 것이 아닌가?
type MyType<T> = T extends infer R ? R : null;
const a: MyType<number> = 123;
console.log(typeof a); //number
// 사실 위와 같은 코드는 아래와 동일하다
type MyType<T> = T extends number ? number : null;
infer 의 활용
-
infer 를 활용하면 특정 타입을 반환하는 타입을 만들 때, 타입이 추가될 때마다 조건부의 union 타입에 타입을 추가할 필요 없이 전달 받은 타입을 추론하여 바로 사용이 가능하여 유틸리티 타입으로 사용이 가능하다
-
간단한 예시
type MyNormalType<T> = T extends number ? number : null;
type MyInferType<T> = T extends infer R ? R : null;
// 기존에 지정한 union 이외의 타입을 지정하려 하면 아래와 같이 에러 발생 -> 따라서 타입이 추가되면 MyNormalType 에 조건부에 union 타입을 지속적으로 추가 필요
const testNormal: MyNormalType<string> = 'string';
// 전달 받은 타입을 infer 로 추론한 R 을 사용하여 타입을 지정하므로, 타입이 추가될 때마다 union 타입을 추가할 필요가 없음. 유틸리티 타입으로 활용 가능
const testInfer: MyInferType<string> = 'string';
- 함수에 적용한 복잡한 예시
// infer 미적용 코드, 특정 타입이 추가될 때마다 유니온 타입에 추가 필요
type FuncReturnType<T extends (...args: any) => any> = string | number; // 유니온 타입
function fn(num: number) {
return num;
}
const a: ReturnType<typeof fn> = 6;
console.log(a); // 6
// infer 적용 코드, 함수의 리턴 타입을 알아서 추론하여 union 추가 필요 없이 리턴 타입 활용 가능
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
function fn(num: number) {
return num.toString();
}
const a: ReturnType<typeof fn> = 'Hello';
** 참고 블로그 ** https://velog.io/@from_numpy/TypeScript-infer (opens in a new tab)
다시 책 내용
- extends 사용시 infer 키워드를 추가하여 타입을 추론하는 방식이 가능하다
// Promise 로 감싸진 배열을 받아서, 해당 배열의 타입을 반환하거나 아닐경우 any 를 반환하는 타입
type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;
const promises = [Promise.resolve('Mark'), Promise.resolve(38)];
type Expected = UnpackPromise<typeof promises>; // string | number
** [p.158] 라우팅을 모르는 사람이 이걸 보고 있을까요? 그리고 라우팅 모르는 분이 여기 예시 코드 이해가 가능할까요? ㅎㅎㅎ ** 내용을 더 잘해줬으면 하는 작은 소망이 들지만... 이 모든 것은 제가 TS 를 잘 모르기 때문에 생기는 이슈일 테니... 공부하겠습니다!
- 실제 배민 예시 코드
type PermissionNames = '기기 정보 관리' | '안전모 인증 관리' | '운행 여부 조회';
type UnpackMenuNames<T extends ReadonlyArray<MenuItem>> =
T extends ReadonlyArray<infer U>
? U extends MainMenu
? U['subMenus'] extends infer V
? V extends ReadonlyArray<SubMenu>
? UnpackMenuNames<V>
: U['name']
: never
: U extends SubMenu
? U['name']
: never
: never;
export type PermissionNames = UnpackMenuNames<typeof menuList>;
** [p.161] 이 코드 뭐죠? Type 에서 재귀가 가능한 거였군요... 그리고 전 남이 이렇게 3항 연산자로 코드 짜오면... 걍 안읽을거 같은데.... ** 메뉴가 중첩 구조로 있는 경우에는 잘 작동하겠지만, 타입을 이렇게 써야하는게 최선일까요?
5.2 템플릿 리터럴 타입 활용하기
- 타입을 특정 문자열로 지정하여 고유한 타입을 만들어 사용하는 방법
- 타입을 특정 문자열로 검증하여 휴먼 에러 방지 및 자동 완성 기능을 통해 개발 생산성 향상 가능
- TS 4.1 버전 부터는 템플릿 리터럴 타입을 지원하여 리터럴 타입을 더욱 확장하여 사용이 가능
type HeadingNumber = 1 | 2 | 3 | 4 | 5;
type HeaderTag = `h${HeadingNumber}`;
- Direction 타입의 경우 모든 상황을 리터럴로 전부 선언하지 않고, 수직 - 수평에 대한 타입을 템플릿 리터럴 타입으로 조합하여 사용이 가능하다
// 전부 선언한 리터럴 타입
type Direction =
| 'top'
| 'topLeft'
| 'topRight'
| 'bottom'
| 'bottomLeft'
| 'bottomRight';
// 수직 - 수평 타입을 템플릴 리터럴로 조합
type Vertical = 'top' | 'bottom';
type Horizon = 'left' | 'right';
type DirectionTemplete = Vertical | `${Vertical}${Capitalize<Horizon>}`;
- 유니온 타입 경우의 수가 너무 많은 경우 TS 의 타입 추론 시간이 너무 오래걸려 에러를 발생 시킬 수 있으므로 적절히 나누어 사용해야 한다
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Chunk = `${Digit}${Digit}${Digit}${Digit}`;
type PhoneNumberType = `010-${Digit}-${Digit}`;
// 위의 타입은 10^4 의 타입을 2개 연속으로 가지는 타입이므로 최종적으로 (10^4)^2 의 경우의 수를 가지게 되는 안좋은 예시가 된다
5.3 커스텀 유틸리티 타입 활용하기
5.3.1 유틸리티 함수를 활용해 styled-components 의 중복 타입 선언 피하기
-
styled-components 를 사용하여 동적 스타일링을 구현하는 경우 styled-components 에 전달하기위한 StyledProps 가 발생
-
하지만 StyledProps 의 경우 컴포넌트에서 받은 Props 의 특정 속성을 그대로 받아오는 것이기 때문에, 따로 선언하여 사용할 경우 코드 중복이 발생한다
-
이러한 상황에서 StyledProps 를 따로 선언하여 사용하는 경우의 코드
-
Props 정확하게 동일한 타입의 StyledProps 를 별도로 선언하여 코드 중복이 발생하며, Props 의 속성이 변경되면 StyledProps 도 동일하게 변경을 해줘야 하는 문제점이 발생한다
// HrComponent.tsx
export type Props = {
height?: string;
color?: keyof typeof colors;
isFull?: boolean;
className?: string;
}
export const Hr: VFC<Props> = ({ height, color, isFull, className }) => {
return <HrComponent height={height} color={color} isFull={isFull} className={class Name} />;
};
// style.ts
type StyledProps = {
height?: string;
color?: keyof typeof Color;
isFull?: boolean;
className?: string;
}
- Pick(타입의 특정 속성만 가져오기), Omit(타입의 특정 속성만 빼고 가져오기)를 사용하여 StyledProps 에 필요한 속성만 Props 에서 가져와서 문제점을 수정한 코드
// HrComponent.tsx
export type Props = {
height?: string;
color?: keyof typeof colors;
isFull?: boolean;
className?: string;
}
export const Hr: VFC<Props> = ({ height, color, isFull, className }) => {
return <HrComponent height={height} color={color} isFull={isFull} className={class Name} />;
};
// style.ts
type UtilityStyledProps = Pick<Props, 'height' | 'color' | 'isFull'>;
5.3.2 PickOne 유틸리티 함수
-
TS 에서는 서로 다른 2개 이상의 객체를 유니온 타입으로 받을 때 타입 검사가 제대로 진행되지 않는 이슈가 발생
-
이런 문제를 해결하기 위해 PickOne 이라는 유틸리티 함수가 사용
-
CreditCard, 또는 Account 중 하나의 타입만 받고 싶은 상황에서 유니온을 사용하였으나 제대로 타입 검사가 이루어지지 않는 케이스
type Account = {
account: string;
};
function withdraw(type: CreditCard | Account) {
// Do sth
}
withdraw({ card: 'hyundai', account: 'hana' }); // Err 가 발생하지 않는다
식별할 수 있는 유니온으로 객체 타입을 유니온으로 받기
- 각각의 타입에 type 이라는 속성을 추가하여 객체를 구분할 수 있도록 처리
- type 에는 유니온이 적용되어 'card', 'account' 를 둘 다 쓸 수 있지만 해당 값이 결정되면 type 검사가 더 명확하게 진행되어 이전과 같이 card, account 속성을 동시에 사용이 불가능해진다
type CreditCard = {
type: 'card';
card: string;
};
type Account = {
type: 'account';
account: string;
};
function withdraw(type: CreditCard | Account) {
// Do sth
}
withdraw({ type: 'card', card: 'hyundai', account: 'hana' }); // account 속성에서 ERR 발생
- 하지만 이러한 경우는 모든 경우에 수의 type 을 전부 넣어줘야 하는 문제 및 해당 함수가 호출 되는 부분에 대한 모든 수정이 필요한 한계가 발생
PickOne 커스텀 유틸리티 타입 구현하기
- TS 에서 제공하는 유틸리티 타입을 활용하여 커스텀 유틸리티 타입을 만들어 해결이 가능
- 유니온에 의해 합집합 처리가 되는 속성에 옵셔널 undefined 를 붙여 사용자가 고의적으로 undefined 를 넣는게 아니면 타입 에러는 발생 시키는 방법으로 해결하기
type CreditCard = {
account?: undefined;
card: string;
};
type Account = {
account: string;
card?: undefined;
};
function withdraw(type: CreditCard | Account) {
// Do sth
}
withdraw({ card: 'hyundai', account: 'hana' }); // Card 를 의도했다면 account 에 undefined 가 아닌 string 값이 왔으므로 ERR 발생, 반대도 성립
- 위의 Case 를 커스텀 유틸리티 타입으로 구현한 타입
** [p.168] 설명에 대한 흐름과, 코드 예시 둘 다 안좋은 케이스가 아닌가 싶습니다. 진짜, 이해를 하는데 시간을 한 3배 쓰는 느낌? ** 대신 불지옥 난이도로 배울 수 있어서 그런가... 확실하게 공부는 됩니다
type CreditCard = {
card: string;
};
type Account = {
account: string;
};
type PickOne<T> = {
[P in keyof T]: Record<P, T[P]> &
Partial<Record<Exclude<keyof T, P>, undefined>>;
}[keyof T];
// 갑자기 여기 조건은 & 로 변경
type CardOrAccount = PickOne<CreditCard & Account>;
function withdraw(type: CardOrAccount) {
// Do sth
}
withdraw({ card: 'hyundai' });
- GPT 가 추천한 PickOne 타입 버전 코드
type CreditCard = {
card: string;
};
type Account = {
account: string;
};
// 합집합으로 모인 타입의 키가 전달 된 타입에 포함이면 해당 속성의 타입으로 반환하고, 포함되지 경우라면 undefined 를 고정으로 가지도록 만드는 타입 구문
type PickOne2<T> = T extends infer U ? { [K in keyof U]: U[K] } : undefined;
type CardOrAccount2 = PickOne2<CreditCard | Account>;
function withdraw2(type: CardOrAccount2) {
// Do sth
}
withdraw2({ card: 'hyundai' });
PickOne 살펴보기
type One<T> = { [P in keyof T]: Record<P, T[P]> }[keyof T];
// 1) P 는 T 객체의 키 값
// 2) 해당 속성값은 Record<P, T[P]> 에 의해 P 라는 키를 가지고 값은 전해진 객체의 값의 타입을 가진다
// 3) 따라서 2)에 의해 전해진 값은 전달된 객체의 키에 값이 Record<P, T[P]> 로 전해진 객체의 유니온 값이 된다
// 4) 최종에서 다시 [keyof T] 의 키값으로 접근하므로 Record<P, T[P]> 의 유니온 값들이 하나로 합쳐지는 효과를 가진다
// 예시
type CreditCard = {
card: string;
};
type Account = {
account: string;
};
type One<T> = { [P in keyof T]: Record<P, T[P]> }[keyof T];
// 위의 코드에서 3)의 상태에서 One 타입의 결과물은 card: Record<'card', string> | account: Record<'age', string> 이라는 유니온 값을 가짐
// 그런데 이러면 속성 값을 둘 다 가져도 걸러내지 못하기 때문에 ExcludeOne 이라는 타입을 합쳐줘서 필요없는 속성에는 undefined 를 강제 시켜줘야함
type ExcludeOne<T> = {
[P in keyof T]: Partial<Record<Exclude<keyof T, P>, undefined>>;
}[keyof T];
// PickOne 은 전달 받은 속성 값을 전부 가진 타입과, 공통되지 않은 속성은 undefined 로 강제되는 타입의 합집합의 형태를 가진다
type PickOne<T> = One<T> & ExcludeOne<T>;
type CardOrAccount = PickOne<CreditCard & Account>;
function withdraw(type: CardOrAccount) {
// do sth
}
withdraw({ card: 'card' });
withdraw({ card: 'card', account: 'account' }); // ERR
** [p.171] type CardOrAccount = PickOne<CreditCard & Account>;
여기서 조건이 | 에서 & 로 변하는데 이걸.... 설명을 스킵하면 되나요? ㅋㅋㅋㅋㅋ
5.3.3 NonNullable 타입 검사 함수를 사용하여 간편하게 타입 가드하기
NonNullable 타입이란?
- 제네릭이 null 또는 undefined 타입일 경우 never 또는 T 를 반환하는 타입, 이를 이용하여 null 이나 undefined 가 아닌 경우를 제외 가능하다
type NonNullable<T> = T extends null | undefined ? never : T;
null, undefined 를 검사하는 isNonNullable 함수
- 해당 함수에 value 로 전달하면 null 이나 undefined 가 아닌 타입으로 타입을 좁힐 수 있다
function isNonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
const nullType: null = null;
const notNullType: string = 'string';
console.log(isNonNullable(nullType));
console.log(isNonNullable(notNullType));
5.4 불변 객체 타입으로 활용하기
5.4.1 Atom 컴포넌트에서 theme style 객체 활용하기
- theme 객체를 통해 색상을 as const 로 선언하여 불변 객체로 선언하여 사용
- 값을 받을 때에는 리터럴을 이용하여 접근하여 사용
interface Props {
fontSize?: string;
backgroundColor?: string;
color?: string;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
}
const Button: FC<Props> = ({ fontSize, backgroundColor, color, children }) => {
return (
<ButtonWrap
fontSize={fontSize}
backgroundColor={backgroundColor}
color={color}
>
{' '}
{children}{' '}
</ButtonWrap>
);
};
// theme 컬러는 props 로 받아온 값을 적용하기 위해 리터럴 접근법 사용
const ButtonWrap = styled.button<Omit<Props, 'onClick'>>`
color: ${({ color }) => theme.color[color ?? 'default']};
background-color: ${({ backgroundColor }) =>
theme.bgColor[backgroundColor ?? 'default']};
font-size: ${({ fontSize }) => theme.fontSize[fontSize ?? 'default']};
`;
타입스크립트 keyof 연산자로 객체의 키값을 타입으로 추출하기
- keyof 연산자를 사용하면 객체의 키 값을 string 또는 리터럴 유니온으로 반환
interface ColorType {
red: string;
green: string;
blue: string;
}
type ColorKeyType = keyof ColorType; // 'red' | 'green' | 'blue'
타입스크립트 typeof 연산자로 값을 타입으로 다루기
- 컬러 객체의 타입 자체를 반환하는 typeof 를 사용
객체의 타입을 활용해서 컴포넌트 구현하기
import { FC } from 'react';
import styled from 'styled-components';
const colors = {
black: '#000000',
gray: '#222222',
white: '#FFFFFF',
mint: '#2AC1BC',
};
const theme = {
colors: {
default: colors.gray,
...colors,
},
backgroundColor: {
default: colors.white,
gray: colors.gray,
mint: colors.mint,
black: colors.black,
},
fontSize: { default: '16px', small: '14px', large: '18px' },
};
// theme 객체의 타입을 추출하여 타입을 만들기
// 그 와중에 typeof keyof 순서 바뀐거 실화냐....
type ColorType = keyof typeof theme.colors;
type BackgroundColorType = keyof typeof theme.backgroundColor;
type FontSizeType = keyof typeof theme.fontSize;
// Props 에 theme 객체의 타입을 추출하여 적용한 타입을 적용
interface Props {
color?: ColorType;
backgroundColor?: BackgroundColorType;
fontSize?: FontSizeType;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
}
const Button: FC<Props> = ({ fontSize, backgroundColor, color, children }) => {
return (
<ButtonWrap
fontSize={fontSize}
backgroundColor={backgroundColor}
color={color}
>
{children}
</ButtonWrap>
);
};
const ButtonWrap = styled.button<Omit<Props, 'onClick'>>`
color: ${({ color }) => theme.color[color ?? 'default']};
background-color: ${({ backgroundColor }) =>
theme.bgColor[backgroundColor ?? 'default']};
font-size: ${({ fontSize }) => theme.fontSize[fontSize ?? 'default']};
`;
** [p.178] typeof keyof 순서가 바뀐거는 에러가 당연히 나는 코드인데 진짜 이 예제코드가 진짜 배민에서 쓰이는가 하는 의문이 듭니다. ** 예제 화면으로 나온거만봐도 TestComponent 인거 보면, 책 쓰려고 급히 짠 코드가 아닌가 하는 생각도 듭니다 ^ㅅ^;;;;;;
5.5 Record 원시 타입 키 개선하기
5.5.1 무한한 키를 집합으로 가지는 Record
- Record 의 첫 타입이 string 인 Category 가 전달이 되므로 foodByCategory 는 이론적으로 무한한 키 집합을 가지게 된다
type Category = string;
interface Food {
name: string;
// ...
}
const foodByCategory: Record<Category, Food[]> = {
한식: [{ name: '제육덮밥' }, { name: '뚝배기 불고기' }],
일식: [{ name: '초밥' }, { name: '텐동' }],
};
foodByCategory['양식']; // Food[] 로 추론
foodByCategory['양식'].map((food) => console.log(food));
// 컴파일 타임에서는 에러가 발생하지 않지만, 실제로는 undefined 값이므로 map 에서 에러가 발생한다
- 따라서
foodByCategory['양식'];
같은 호출을 해도 컴파일 타임에서는 문제를 일으키지 않는 문제가 발생한다
5.5.2 유닛 타입으로 변경하기
- 키를 유닛 타입으로 변경하여 제한하는 방법으로 해결이 가능하다
type Category = '한식' | '일식';
interface Food {
name: string;
// ...
}
const foodByCategory: Record<Category, Food[]> = {
한식: [{ name: '제육덮밥' }, { name: '뚝배기 불고기' }],
일식: [{ name: '초밥' }, { name: '텐동' }],
};
foodByCategory['양식']; // 컴파일 타임에서 에러 발생
5.5.3 Partial 을 활용하여 정확한 타입 표현하기
- 키가 무한으로 추론이 되는 상황에서 Partial 를 활용하면 해당 값이 undefined 임을 표현할 수 있다
- Partial 을 사용하면 특정 타입의 키를 전부 옵셔널로 변경하게 되므로, 특정 객체의 모든 키 값을 TS 가 추론하게 하게되어 없는 키 값의 경우 undefined 가 가능하다고 판단 -> undefined 값에 대한 접근에 대한 에러를 띄우게 된다
type Category = string;
interface Food {
name: string;
// ...
}
type PartialRecord<K extends string, T> = Partial<Record<K, T>>;
const foodByCategory: PartialRecord<Category, Food[]> = {
한식: [{ name: '제육덮밥' }, { name: '뚝배기 불고기' }],
일식: [{ name: '초밥' }, { name: '텐동' }],
};
foodByCategory['양식']; // Food[] 또는 undefined 로 추론
foodByCategory['양식'].map((food) => console.log(food));
// undefined 가 될 수 있다고 추론 되었으므로, 컴파일 타임에서 에러 발생