5장. 타입 활용하기
5.1 조건부 타입
Condition ? A : B
1. extends와 제네릭을 활용한 조건부 타입
extends 키워드
- 타입을 확장할 때,
- 타입을 조건부로 설정할 때
- 제네릭 타입에서는 한정자 역할로도 사용
T extends U ? X : Y
T를 U에 할당할 수 있으면 X 타입, 아니면 Y 타입
2. 조건부 타입을 사용하지 않았을 때의 문제점
3. extends 조건부 타입을 활용하여 개선하기
4. infer를 활용해서 타입 추론하기
(2~4 다시보기)
5.2 템플릿 리터럴 타입 활용하기
유니온 타입을 사용하여 변수 타입을 특정 문자열로 지정할 수 있다.
컴파일타임의 변수에 할당되는 타입을 특정 문자열로 정확하게 검사하여 휴먼 에러 방지할 수 있고, 자동 완성 기능을 통해 개발 생산성을 높일 수 있다.
타입스크립트 4.1부터 지원
type Vertical
type Horizon
type Direction = Vertical | `${Vertical}${Capitalize<Horizon>}`
주의할 점
- 타입스크립트 컴파일러가 유니온을 추론하는 데 시간이 오래 걸리면 비효율적이기 때문에 타입스크립트가 타입을 추론하지 않고 에러를 내뱉을 때가 있다.
5.3 커스텀 유틸리티 타입 활용하기
1. 유틸리티 함수를 활용해 styled-components의 중복 타입 선언 피하기
2. PickOne 유틸리티 함수
유니온의 합집합 특성으로 card, account 속성을 모두 가진 객체도 허용되는 문제가 발생한다.
type Card = {
card: string;
}
type Account = {
account: string;
}
function withdraw(type: Card | Account){
console.log(type) // {card: 'hyundai', account: 'hana'}
}
// 다음과 같인 card, account 속성이 모두 포함되어도 에러가 나지 않는다.
// 유니온은 합집합이 되기 때문이다.
withdraw({card: 'hyundai', account: 'hana'});
// 참고.
// function withdraw2(type: Card | Account){
// // 'Card' 형식에 'account' 속성이 없습니다.ts(2339)
// // 공통 속성에만 접근이 가능하다.
// console.log(type.account)
// }
// withdraw2({card: 'hyundai'});
#### 식별할 수 있는 유니온으로 객체 타입을 유니온으로 받기 → 일일이 type을 다 넣어줘야 하는 불편함
type Card = {
type: 'card';
card: string;
}
type Account = {
type: 'account';
account: string;
}
withdraw({type: 'card', card: 'hyundai'});
withdraw({type: 'account', account: 'hana'});
PickOne 커스텀 유틸리티 타입 구현하기
선택하고자 하는 하나의 속성을 제외한 나머지 값을 옵셔널 타입 + undefined로 설정하면 원하고자 하는 속성만 받도록 구현할 수 있다.
type One<T> = { [P in keyof T]: Record<P, T[P]> }[keyof T];
/* T는 객체로 가정
- 1) [P in keyof T]에서 P는 T객체의 키값
- 2) Record<P, T[P]>는 P 타입을 키로 가지고, value는 P를 키로 둔 T 객체의 값의 레코드 타입
- 3) 따라서 { [P in keyof T]: Record<P, T[P]> }에서 키는 T 객체의 키 모음, value는 해당 키의 원본 객체 T
- 4) 3번 타입에서 다시 [keyof T]의 키값으로 접근하기 때문에 최종 결과는 전달받은 T와 같다.
*/
/*
* type Card = { card: string };
* const one: One<Card> = { card: 'hyundai" };
*
* 3) { card: { card: 'hyundai" } }
* 4) { card: 'hyundai" }
*/
type ExcludeOne<T> = { [P in keyof T]: Partial<Record<Exclude<keyof T, P>, undefined>> }[keyof T];
/*
- 1) [P in keyof T]에서 P는 T객체의 키값
- 2) Exclude<keyof T, P>는 T 객체가 가진 키값에서 P 타입과 일치하는 키값을 제외 (A)
- 3) Record<A, undefined>는 키로 A 타입을, 값으로 undefined 타입을 갖는 레코드 타입. 즉, 전달받은 객체 타입을 모두 { [key]: undefined } 형태로 만든다. (B)
- 4) Partial<B>는 B 타입을 모두 옵셔널로 만든다. { [key]?: undefined }
- 5) 최종적으로 [P in keyof T]로 매핑된 타입에서 동일한 객체의 키값인 [keyof T]로 접근하기 때문에 4번 타입이 반환된다.
*/
// type PickOne<T> = One<T> & ExcludeOne<T>;
type PickOne<T> = {
[P in keyof T]: Record<P, T[P]> & Partial<Record<Exclude<keyof T, P>, undefined>>;
}[keyof T];
→ 전달된 T 타입의 1개의 키는 값을 가지고 있으며, 나머지 키는 옵셔널한 undefined 값을 가진 객체
[170p]
3. NonNullable 타입 검사 함수를 사용하여 간편하게 타입 가드하기
NonNullable 타입: 제네릭으로 받는 T가 null 또는 undefined일 때 never 또는 T를 반환하는 타입
type NonNullable<T> = T extends null | undefined ? never : T;
NonNullable 함수: 매개변수가 null 또는 undefined라면 false를 반환한다.
function NonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
// 사용하는 쪽에서 true가 반환된다면 넘겨준 인자는 null, undefined가 아닌 타입으로 타입 가드가 된다/타입이 좁혀진다.
Promise.all을 사용할 때 NonNullable 적용하기
5.4 불변 객체 타입으로 활용하기
상숫값을 관리할 때 객체를 열린 타입으로 사용하는 예
const colors = {
red: "#F45452",
green: "#0C952A",
blue: "#1A7CFF",
};
// string으로 설정하면 colors에 어떤 값이 추가될지 모르기 때문에 getColorHex 함수의 반환 값은 any가 된다.
const getColorHex = (key: string) => colors[key];
1. Atom 컴포넌트에서 theme style 객체 활용하기
props 타입이 string이면 키 값이 자동 완성되지 않으며, 잘못된 키값을 넣어도 에러가 발생하지 않게 된다.
keyof, typeof 연산자로 theme 객체 타입을 구체화해서 해결해보자.
타입스크립트 keyof 연산자로 객체의 키값을 타입으로 추출하기
interface ColorType {
red: string;
green: string;
blue: string;
}
type ColorKeyType = keyof ColorType; // 'red' | 'green' | 'blue'
타입스크립트 typeof 연산자로 값을 타입으로 다루기
const colors = {
red: "#F45452",
green: "#0C952A",
blue: "#1A7CFF",
};
type ColorsType = typeof colors;
/* {
red: string;
green: string;
blue: string;
} */
객체의 타입을 활용해서 컴포넌트 구현하기
import React, { 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
},
backgroundColors: {
default: colors.white,
gray: colors.gray,
mint: colors.mint,
black: colors.black,
},
fontSize: {
default: "16px",
small: "14px",
large: "18px",
},
};
type ColorType = keyof typeof theme.colors;
type BackgroundColorType = keyof typeof theme.backgroundColors;
type FontSizeType = keyof typeof theme.fontSize;
interface Props {
color?: ColorType;
backgroundColor?: BackgroundColorType;
fontSize?: FontSizeType;
children?: React.ReactNode;
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.colors[color ?? "default"]};
background-color: ${({ backgroundColor }) =>
theme.backgroundColors[backgroundColor ?? "default"]};
font-size: ${({ fontSize }) => theme.fontSize[fontSize ?? "default"]};
`;