728x90
반응형
1. Type assertion 대신 Type Guard로 타입 좁히기
- as(type assertion)대신 type guard를 사용하는 것이 대부분 더 안전합니다.
- as(type assertion)의 경우 type 체크의 블랙박스를 만들고 운영 이슈로 이어질 수 있습니다.
const dog = {} as Dog;
// ❌ 런타임 에러가 나기 쉽다.
dog.bark();
interface Animal {
type: 'dog' | 'bird'
}
type AnimalType = Animal['type']
interface Dog extends Animal {
type: 'dog';
bark: VoidFunction
}
interface Bird extends Animal {
type: 'bird';
fly: VoidFunction
}
const eagle:Bird = {
type: 'bird',
fly:() => console.log('훨훨')
}
const animal:Animal = eagle;
// 🚫 타입 단언 사용
(animal as Bird)?.fly()
// ✅ 타입 가드문법을 확용해 타입 좁히기
const isBird = (animal:Animal): animal is Bird => animal.type === 'bird'
if(isBird(animal)){
// 타입이 확정되어 타입 추론을 기대할 수 있다.
animal.fly()
}
const animalList:Animal[] = [ ... ];
return (<>
animalList.map(animal =>
isBird(animal) ? <Brid {...animal}/>: <Dog {...animal} />
)<>
)
Type Assersion은 언제 써야할까?
컴파일러가 타입을 추론할 수 없는 경우 사용합니다. 컴파일러에게 이 변수의 타입을 보장한다고 말하는 것과 같은 의미 입니다. 즉 컴파일러는 type assertion된 변수에 대해 아무런 검사를 하지 않고 그냥 믿습니다. 대부분의 경우 제네릭 혹은 타입 가드로 표현할 수 있으며 이런 방법이 더 안전합니다.
- e.g. 런타임 인풋, web storage, JSON.parse
// ❌ null이 가능한 HTMLDivElement | null 이지만 타입 단언에 의해 가려졌다.
const divElement = document.querySelector(".wrapper") as HTMLDivElement;
// ✅ 제네릭을 허용하는 경우 제네릭을 사용하자
const divElement = document.querySelector<HTMLDivElement>(".wrapper");
// HTMLDivElement | null
// ✅ 보다 확실하게 타입을 보장할 수 있다.
const isDivElement = (element: Element | null): element is DivElement => {
return element?.tagName === 'DIV'
}
// ✅ querySelector에서 반환된 엘리먼트가 div라는 것을 확신할 수 있다.
if(isDivElement(divElement)){
}
2. Type Assertion 대신 Assertion Function쓰기
💡 Assertion Function 은 알려지지 않은 입력의 type을 식별할 수 있는 런타임 체크 함수다.
런타임 인풋과 같이 입력받는 데이터 타입에 대한 보장을 할 수 없는 경우, 런타임에서 타입을 보장하기 위한 방법으로 Assersion Function 을 사용할 수 있다.
타입 가드와 비슷한 방식이다.
type User = {
age: number;
name: string;
};
function printAge(input: unknown) {
// ❌ type assertion으로 타입에러를 무력화했지만 input의 타입을 보장할 수 없다.
console.log((input as User).age);
}
type User = {
age: number;
name: string;
};
function assertUser(input: unknown): asserts input is User {
const isObject = input && typeof input === 'object';
if (isObject) {
const hasAge = 'age' in input && typeof input.age === 'number';
const hasName = 'name' in input && typeof input.name === 'string';
if (!hasAge) {
throw new Error('User does not have an age.');
}
if (!hasName) {
throw new Error('User does not have a name.');
}
}
throw new Error('Input is not an object');
}
function printAge(input: unknown) {
assertUser(input);
// ✅ type assertion 함수가 input의 타입을 보장해준다.
console.log(input.age);
}
3. Generic 함수처럼 사용하기
generic 문법은 특정 타입에서 내부의 타입을 추론하거나 컴파일러에게 타입을 전달하는 목적으로 사용 할 수 있습니다.
Split
type Split<
S extends string,
Indicator extends string = '.',
> = S extends `${infer T}${Indicator}${infer U}`
? [T, ...Split<U, Indicator>]
: [S];
// ["foo", "bar", "baz"]
type S = Split<'foo.bar.baz'>;
// ["foo", "bar", "baz"]
type S2 = Split<'foo-bar-baz', '-'>;
Join
type BirdArray = ['chicken', 'eagle', 'duck'];
type ConvertString<T> = T extends string ? T : T extends number ? `${T}` : ``;
type JoinArray<
T extends Array<unknown>,
Indicator extends string = '.',
Result extends string = '',
> = T extends [infer First, ...infer RestArray]
? JoinArray<
RestArray,
Indicator,
Result extends ''
? ConvertString<First>
: `${Result}${Indicator}${ConvertString<First>}`
>
: Result;
// 'chicken.eagle.duck'
type JoinBird = JoinArray<BirdArray, '.'>;
Pop
type BirdArray = ['chicken', 'eagle', 'duck'];
/**
* 배열의 마지막 아이템
*/
type Last<T extends string[]> = T extends [...infer _, infer LastItem]
? LastItem
: never;
/**
* 배열의 첫번째 아이템
*/
type Pop<T extends any[]> = T extends [infer First, ...infer Rest]
? First
: never;
/**
* 첫번째 아이템 제거
*/
type RemoveFirst<T extends any[]> = T extends [infer First, ...infer Rest]
? Rest
: never;
type FirstBird = Pop<BirdArray>; // chicken
type RestBird = RemoveFirst<BirdArray>; // eagle, duck
type LastBird = Last<BirdArray> // duck
Repeat
type Repeat<
Value extends string,
Count extends number,
Result extends string[] = [],
> = Result['length'] extends Count
? JoinArray<Result, ''>
: Repeat<Value, Count, [Value, ...Result]>;
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type TwoDigit = Repeat<Digit, 2>; // 00~99
4. Generic을 이용한 컴포넌트 타입 추론
const AnimalList = (props: {
items: Aniaml[];
onClick: (item: Animal) => void
}) => (
<>{props.items.map(item => <li onClick={onClick}>{item.type}</li>}</>
);
/** ❌ birdList를 넣었는데 onClick의 매개변수가 bird인 것을 알지 못한다 */
<AnimalList
items={birdList}
onClick={animal => animal?.fly()}
/>
onClick에 전달되는 매개변수의 타입을 추론하고 싶다면? ⇒ 컴포넌트에 제네릭 뿌리기
const AnimalList = <ItemType extends Aniaml>(props: {
items: ItemType[];
onClick: (item: ItemType) => void
}) => (
<>{props.items.map(item => <li onClick={onClick}>{item.type}</li>}</>
);
/** ✅ 제네릭덕분에 onClick의 매개변수가 bird라는 것을 알 수 있다 */
<AnimalList
items={birdList}
onClick={brid => brid.fly()}
/>
여러 태그의 기능을 포괄하는 공통 컴포넌트를 개발할 때 베이스가 되는 태그를 지정하고 싶을 때 as 를 통해 지정하고 베이스 태그의 attribute를 추론하도록 할 수있다.
<Text as="p">REACT</Text>
// as의 타입으로 Anchor의 타입을 추론할 수 있다.
// Button의 베이스 태그가 a임으로 href이 자동완성된다.
<Button as='a' href='...'>REACT</Button>
any 대신 unkown 사용하기
💡
any
- 모든 타입이 가능하다
- 정의가 필요하지 않다.
- 어떤 타입을 사용해도 컴파일 에러가 나지 않는다.
unknown
- 모든 타입이 가능한 것은 아니지만 어떤 타입인지 알 수 없다.
- 정의가 필요하다정의하지 않으면 컴파일 에러가 발생한다.
대표적인 unknown의 사용 사례
try{
// 1️⃣ NetworkError 발생 가능
const list = await fetchList();
// 컴포넌트에 맞춰 데이터 구조 변형
// 2️⃣ ReffenceError, TypeError 발생 가능
return list.map(convertItem);
}catch(error:any){
// ❌ error가 Axios에러임을 가정하고 있음에도 컴파일러가 아무런 제제를 하지 않는다.
// 만약 error가 ReffenceError면 런타임에러가 발생하게 된다.
const errorType = error?.response?.data?.content?.type;
if(errorType){
captureException(error);
}
}
try{
// 1️⃣ NetworkError 발생 가능
const list = await fetchList();
// 컴포넌트에 맞춰 데이터 구조 변형
// 2️⃣ ReffenceError, TypeError 발생 가능
return list.map(convertItem);
}catch(error: unkown){
// ✅ 컴파일러가 타입 정의를 요구한다.
// 구체적 타입 정의를 요구함으로써 런타임에러가 발생하는 것을 피할 수 있다.
// const errorType = error?.response?.data?.content?.type;
if(error instanceof AxiosError){
// unkown이 AxiosError로 추론됨
const errorType = error?.response?.data?.content?.type;
}else if (error instanceof Error){
// unkown이 Error로 추론됨
console.log(error.message)
}
}
5. 옵셔널 프로퍼티 정의하기
BAD
interface Product {
id: string
type: 'digital' | 'physical'
/** type:physical 경우에 필수 */
weightInKg?: number
/** type:digital 경우에 필수 */
sizeInMb?: number
}
const digitalProduct:Product = await featchProduct();
if(digitalProduct.type === 'digital'){
// 🚫 sizeInMb가 존재함을 컴파일러가 알지 못해 에러 발생
console.log(digitalProduct.sizeInMb)
}
GOOD
interface Product {
id: string;
type: 'digital' | 'physical';
}
interface DigitalProduct extends Product {
type: 'digital';
sizeInMb: number;
}
interface PhysicalProduct extends Product {
type: 'physical';
weightInKg: number;
}
type ProductList = Array<(DigitalProduct | PhysicalProduct)>;
const productList = await featchProduct<ProductList>();
productList.forEach(product => {
if(product.type === 'digital'){
// ✅ product가 DigitalProduct을 추론함
console.log(digitalProduct.sizeInMb)
}
})
- 각 타입일 경우 필수로 전달되는 프로퍼티에 대해 type이 지정되는 순간 필수로 존재함을 보장받음으로서 코드가 안전해진다
if(isDigitalProduct(product)){
console.log(product.weightInKg)
}
6. 템플릿 리터럴을 이용한 타입 추론
- webView 메세지 통신에서 type별 파라미터 타입의 추론 가능
Date() 생성자 - JavaScript | MDN
Date 생성자는 시간의 특정 지점을 나타내는 Date 객체를 플랫폼에 종속되지 않는 형태로 생성합니다. Date 객체는 1970년 1월 1일 UTC(국제표준시) 자정으로부터 지난 시간을 밀리초로 나타내는 UNIX
developer.mozilla.org
템플릿 리터럴을 이용한 타입 좁히기
const afterThreeMonth = (dateString: string) => {
const date = new Date(dateString);
date.setMonth(date.getMonth() + 3);
return date.toISOString();
};
afterThreeMonth('2023-07-31T00:00:00') // '2023-10-30T15:00:00.000Z'
afterThreeMonth('2023-7-31T00:00:00') // Error
afterThreeMonth('2023.07.31T00:00:00') // Error
type DateString = `${number}-${number}-${number}T${number}:${number}:${number}`;
const afterThreeMonth = (dateString: DateString) => {
const date = new Date(dateString);
date.setMonth(date.getMonth() + 3);
return date.toISOString();
};
템플릿 리터럴을 활용한 유틸 타입
type Events = 'click' | 'focus' | 'move' | 'press';
type HandlerNames = `on${Capitalize<Events>}`;
// onClick | onFocus
type CommonHandlers = {
[Name in HandlerNames]: (event: Event) => void;
};
type CommonElement = CommonHandlers & {
id: string;
name?: string;
};
type ExtractEvent<Props extends object> = Exclude<
{
[Name in keyof Props]: Name extends `on${infer R}`
? Uncapitalize<R>
: never;
}[keyof Props],
undefined
>;
// Props에서 이벤트명 추출 'click' | 'focus' | 'move' | 'press'
type EventList = ExtractEvent<CommonElement>
메세지 파라미터 타입 추론
아래 상황에서 MESSAGE_TYPE.PURCHASE 에 매칭되는 파라미터를 입력하도록 타입을 추론할 수 있다.
customPostMessage({
type:MESSAGE_TYPE.PURCHASE
params:{
...
}
})
7. 중복 타입 정의 피하기
1. 확장하기
interface Brid {
eat: () => void
sleep: () => void;
fly: ()=> void;
}
interface Dog {
eat: () => void
sleep: () => void;
bark: ()=> void;
}
type Aniaml = Brid | Dog;
const animalList = fetchAnimal<Aniaml[]>();
interface CommonAnimalProperty {
eat: () => void
sleep: () => void;
}
interface Brid extends CommonAnimalProperty {
fly: ()=> void;
}
interface Dog extends CommonAnimalProperty {
bark: ()=> void;
}
type Aniaml = Brid | Dog;
const animalList = fetchAnimal<Aniaml[]>();
interface CommonAnimalProperty {
eat: () => void
sleep: () => void;
}
/**
* 잠을 자지는 않지만 나머지 특성은 모두 같다
*/
interface NonSleepAnimal extends Omit<CommonAnimalProperty, 'sleep'> {
}
2. 참조하기
// 대략 30개되는 복잡한 복합 타입
type ComplexType = ...;
export interface ComplexProps {
type:ComplexType
}
export const ComplexComponent = (props: ComplexProps) => {
...
}
import { ComplexProps } from 'ComplexComponent';
// 3'th에서 ComplexType을 export 안 해줌
type ComplexType = ComplexProps['type']
import { ComplexComponent } from 'ComplexComponent';
const ComplexProps = ComponentProps<typeof ComplexComponent>
type ComplexType = ComplexProps['type']
728x90
반응형
'typescript' 카테고리의 다른 글
타입스크립트의 공변성과 반변성 (2) | 2024.07.13 |
---|---|
any 타입이 유용한 경우 (1) | 2024.07.13 |
Generic을 활용한 복잡한 구조의 타입 추론하기 (0) | 2024.07.05 |