2장에선 기본적인 타입과 타입시스템에 대해서 알아봤다면, 3장에서는 자바스크립트 자료형에서 표현되지 않은 자료형에 대해서 설명한다. 존재는 했지만 동적 타이핑 특성 상 표현할 필요성이 없던 자료형들을 타입스크립트에서 정적 타이핑을 하기 위해 정의가 된 애들이다.
타입스크립트의 타입 계층 구조
타입스크립트만의 독자적 타입
1. any 타입
자바스크립트에 존재하느 모든 값을 오류 없이 받을 수 있음.
- 개발 단계에서 임시로 값을 지정해야 할 때
- 어떤 값을 받아올지 또는 넘겨줄 지 알 수 없을 때
ex) 외부라이브러리,웹 API요청에 따라 다양한 값을 반환하는 API, 콜백함수 등등 - 값을 예측할 수 없을 때 암묵적으로 사용
ex) 외부라이브러리나 웹 API요청에 따라 다양한 값을 반환하는 API
2. unknown타입
아직 무엇이 할당될 지 모를 때 사용. any타입과 유사한데 비교하면 아래와 같다.
any | unknown |
-어떤 타입이든 ANY에 할당가능 -any 타입을 어떤 타입으로도 할당 가능(never제외) |
-어떤 타입이든 unknown 타입에 할당 가능 -unknown 타입은 any타입 외에 다른 타입으로 할당 불가능 |
any 와 추가적인 차이점은 할당하는 시점에서는 에러가 발생하지 않으나, 실행시에 에러가 발생한다.
즉, 어떠한 타입이든 가져올 수 이찌만 개발자에게 엄격한 타입검사를 강제한다.
배민팀에서는 강제 타입 캐스팅을 할 때 쓰거나, 에러가 발생한다는 점 덕에 any보다 선호한다고 했다.
3. void 타입
함수에서 반환 결과가 없는 경우를 말한다.
자바스크립트에서는 반환하는 값이 없는 함수의 경우 undefined 가 반환되는데, 타입스크립트에서 void는 undefined는 아니다.
void에는 null과 undefined가 할당가능하지만 ,strictNullChecks를 세팅할 경우 에러가 뜨고 추천되지 않는 방식이다.
4. never 타입
함수와 관련해서 쓰는데, 값을 반환할 수 없는 경우 ( 반환 결과가 없는 것과 다름)
크게 2가지 경우로 나뉘는데
- 에러를 던지는 경우
- 무한히 함수가 실행되는 경우
5. Array 타입
자바스크립트에서도 Object.prototype.toString.call(..) 연사자로 확인할 수 있는데 이 함수는 객체의 인스턴스까지 알려주기 때문에 나오는 것이다.
var arr = [];
console.log(Object.prototype.toString.call(arr)); //[object Array]
자바스크립트에서 존재하는 타입이라 생각할 수 있는데 구분하는 이유는 아래와 같다.
- 자바스크립트에서는 배열을 객체에 속하는 타입으로 분류함
- 타입스크립트에서 Array라는 타입을 사용하기 위해서는 타입스크립트의 특수한 문법을 다뤄야 함
타입 제한하기
자바스크립트에서는 배열안에 들어갈 값에 제한이 없다.( 다양한 값이 들어갈 수 있음)
하지만 이러한 성격은 정적 타이핑과 잘 부합하지 않고, 실제로 다른 언어는 배열의 원소로 하나의 타입만 허용한다.
이를 제한하기 위해서 자료형+ [] 형식으로 배열 타입을 선언할 수 있고, 여러가지 선언할 경우 유니온 타입을 사용할 수 있다.
const arr = [];
console.log(Object.prototype.toString.call(arr));
const array: Array<number> = [1, 2, 3];
const array2: number[] = [1, 2, 3];
const unionArr: Array<number | string> = [1, 'string'];
const unionArr2: (number | string)[] = [1, 'string'];
길이 제한하기
자바등 다른 정적 타입의 경우 길이도 제한하는 경우가 많다. 이때 사용하는 것이 튜플 타입이다.
대괄호 안에 들어가는 원수의 수가 튜플이 가질 수 있는 원소의 개수다
const tuple: [number] = [1];
const tuple2: [number, string] = [1, 'string'];
가장 대표적인 예시는 react의 useState의 경우이다.
const [username, setUsername] = useState('');
이렇게 튜플 타입을 사용할 경우 객체의 구조 분해 할당을 사용하는 것과 다르게 변수 이름에서 자유로워진다.
즉 유연성이 향상된다.
6 enum 타입
열거형이라고도 부르는 특수한 타입이다.
일일이 값을 할당할 수도 있지만 ,누락된 번호의 경우에는 이전 멤버 값의 숫자를 1씩 늘리며 자동 할당한다.
enum Language {
Typescript, //0
Javascript, //1
Java, //2
Python = 'Python',
Kotlin = 300,
Rust, //301
}
열거형은 그 자체로 변수 타입으로 지정할 수 있다. 따라서 코드 가독성을 높인다.
enum ItemStatusType {
HOLd = 'HOLD', //배송보류
READY = 'READY', //배송 준비
DELIVERING = 'DELIVERING', // 배송 중
DELIVERED = 'DELIVERED', // 배송완료
}
const checkItemAvailable = (itemStatus: ItemStatusType) => {
switch (itemStatus) {
case ItemStatusType.HOLd:
case ItemStatusType.READY:
case ItemStatusType.DELIVERING:
return false;
case ItemStatusType.DELIVERED:
defalut: return true;
}
};
위의 예시와 같이 얻을 수 있는 효과로는
- 타입 안정성 (명시되지 않은 값은 받을 수 없다)
- 명확한 의미 전달과 높은 응집력 : 타입이 어떤 값을 다루는 지 명확하고, 아이템 상태에 대한 값을 모아 놓아서 응집력이 높다
- 가독성 : 응집도가 높기 떄문에 말하고자 하는 바가 명확하다.
다만 열거형의 경우에는 범위를 넘어서는 경우 접근해도 에러가 생기지 않는다. const enum으로 열거형 선언을 할 경우 역방향으로의 접근을 허용하지는 않지만, 문자열로 사용하는 것이 더 안전하다.
(교제에서는 에러는 발생하지 않는다고 하는데, 에러가 생김,,)
const enum NUMBER {
ONE = 1,
TWO = 2,
}
enum NUM {
ONE = 1,
TWO = 2,
THREE,
}
const notMyNumber: NUM = 100; //'100' 형식은 'NUMBER' 형식에 할당할 수 없습니다.ts(2322)
const myNumber: NUMBER = 100; //'100' 형식은 'NUMBER' 형식에 할당할 수 없습니다.ts(2322)
const enum STRING_NUMBER {
ONE = 'ONE',
TWO = 'TWO',
}
const myStringNum: STRING_NUMBER = 'THREE'; //'"THREE"' 형식은 'STRING_NUMBER' 형식에 할당할 수 없습니다.ts(2322)
또한 추가적인 문제점은 열거형은 타입스크립트 코드가 자바스크립트 코드로 변환되어 트리쉐이킹 과정에서 사용하지 않는 코드로 인식하지 못하는 경우가 발생할 수 있다.
(function (NUM) {
NUM[NUM["ONE"] = 1] = "ONE";
NUM[NUM["TWO"] = 2] = "TWO";
NUM[NUM["THREE"] = 3] = "THREE";
})(NUM || (NUM = {}));
var notMyNumber = 100; //'100' 형식은 'NUMBER' 형식에 할당할 수 없습니다.ts(2322)
var myNumber = 100; //'100' 형식은 'NUMBER' 형식에 할당할 수 없습니다.ts(2322)
var myStringNum = 'THREE'; //'"THREE"' 형식은 'STRING_NUMBER' 형식에 할당할 수 없습니다.ts(2322)
타입 조합
1. 교차 타입
여러 가지 타입을 결합하여 하나의 단일 타입을 만들 수 있다
A & B는 A와 B 모두를 만족하는 타입이다.
type ProductItem = {
id: number;
name: string;
type: string;
price: number;
};
type ProductItemWithDiscount = ProductItem & { discountAmount: number };
const item: ProductItem = { id: 1, name: '굽네', type: '치킨', price: 20000 };
const disItem: ProductItemWithDiscount = {
id: 1,
name: '굽네',
type: '치킨',
price: 20000,
discountAmount: 500,
};
2. 유니온 타입
A와 B중 하나를 만족하는 타입. 그렇기 때문에 둘 중 하나만 해당하는 경우에는 컴파일 에러가 뜬다.
type CardItem = {
id: number;
name: string;
type: string;
imageUrl: string;
};
type PromotionEventItem = ProductItem | CardItem;
const printPromotionItem = (item: PromotionEventItem) => {
console.log(item.name);
// console.log(item.imageUrl);// 컴파일에러
};
3. 인덱스 시그니처
특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 있을 때 사용하는 문법
[Key:K ] :T 꼴로 타입을 명시해주면 된다.
타입을 추가로 명시할 수 있는데 이 경우에는 인덱스 시그니처에 포함되어야 한다.
interface IndexSignatureEx2 {
[key: string]: number | boolean;
length: number;
isValid: boolean;
// name: string;//에러
}
4. 인덱시드 엑세스 타입
다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용된다.
type Example = {
a: number;
b: string;
c: boolean;
};
type IndexedAccess = Example['a'];
type IndexedAccess2 = Example['a' | 'b'];
type IndexedAccess3 = Example[keyof Example];
const indexedConst: IndexedAccess = 3;
const indexedConst2: IndexedAccess2 = 3;
const indexedConst2n1: IndexedAccess2 = '3';
const indexedKeyOf1: IndexedAccess3 = '3';
const indexedKeyOf2: IndexedAccess3 = 3;
const indexedKeyOf3: IndexedAccess3 = false;
배열의 타입을 조회할 때도 쓸 수 있다.
const PromotionList = [
{ type: 'product', name: 'chicken' },
{ type: 'product', name: 'pizza' },
{ type: 'card', name: 'cheer-up' },
];
const PromotionList2 = [
{ type: 'product', name: 'chicken' },
{ type: 'product', name: 'pizza' },
{ type: 3, name: 'cheer-up' },
];
type ElementOf = (typeof PromotionList)[number];
const promotionData: ElementOf = { type: 'product', name: 'chicken' };
type ElementOf2 = (typeof PromotionList2)[number]; //type ElementOf2 = { type: string; name: string;} | { type: number; name: string;}
5. 맵드 타입
map은 보통 유사한 형태를 가진 여러 항목의 목록 A를 변환된 항목의 목록 B로 바꾸는 것을 의미한다.
type MapExample = {
a: number;
b: string;
c: boolean;
};
type Subset<T> = {
[K in keyof T]?: T[K];
};
const aExample: Subset<MapExample> = { a: 3 };
const aExample2: Subset<MapExample> = { b: 'string' };
const aExample3: Subset<MapExample> = { a: 3, b: 'string' };
a,b,c가 필수 이던 타입을 아래와 같이 optional 하게 바꿀 수도 있고,
-readonly -? 를 통해서 readonly 와 옵셔널을 제거할 수도 있다. 불필요한 반복을 줄이기 위해 사용한다.
type ReadOnlyEx = {
readonly a: number;
readonly b: string;
};
type CreateMuatable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type ResultType = CreateMuatable<ReadOnlyEx>; //{ a: number; b: string;}
6. 템플릿 리터럴 타입
템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법
type Stage = 'init' | 'select-image';
type StageName = `${Stage}-stage`; //"init-stage" | "select-image-stage"
새로운 유니온 타입을 만들 수 있다.
7. 제네릭
정적 언어에서 다양한 타입간의 재사용을 높이기 위해 사용하는 문법
제네릭안에는 어떠한 타입이든 올 수 있지만, 생성 시점에 원하는 타입을 특정할 수 있다.
아래 예시는 string만 올 수 있는 배열을 제네릭 문법을 통해 생성하였다.
type ExampleArrayType<T> = T[];
const array1: ExampleArrayType<string> = ['피자', '치킨'];
타입 추론이 가능한 경우에는 타입 명시를 생략할 수 있다.
function exampleFunc<T>(arg: T): T[] {
return new Array(3).fill(arg);
}
function exampleFunc2<T>(arg: T): number {
return arg.length; // 에러가 생김 'T' 형식에 'length' 속성이 없습니다
}
이때 어떤 타입이든 올 수 있기에 특정 타입에만 존재하는 메서드를 제네릭을 사용할 떄 함꼐 쓰면 안된다.
또한 화살표함수에서 JSX문법에서의 꺽쇠괄호와 혼동이 생길 수 있으므로 해당 경우에는 extends 키워드를 사용하여야 한다.
const arrowExampleFunc2 = <T extends {}>(arg: T): T[] => {
return new Array(3).fill(arg);
};
제네릭의 사용처는 다양하다
- 함수의 제네릭
함수의 매개변수나 반환값에 다양한 타입을 넣고 싶을 때 - 호출 시그니처의 제네릭
호출 시그니처란 타입스크립트의 함수 타입 문법으로 함수의 매개변수와 반환 타입을 미리 선언하는 것을 말한다.
// 배민선물하기 예시 interface useSelectPaginationProps<T> { categoryAtom: RecoilState<number>; filterAtom: RecoilState<string[]>; sortAtom: RecoilState<SortType>; fetcherFunc: ( props: CommonListRequest ) => Promis<DefaultResponse<ContentListResponse<T>>>; }
- 제네릭 클래스
외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스
클래스 이름 뒤에 타입 매개변수인 <T>를 선언해준다.
class GenericNumber<NumType> { zeroValue: NumType; add: (x: NumType, y: NumType) => NumType; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function (x, y) { return x + y; };
- 제한된 제네릭
타입 매개변수에 대한 제약 조건을 설정하는 기능을 말한다.
유니온 타입을 상속해서 유연성을 잃지 않으면서 타입 제약을 할 수 있다.
아래에는 extends로 제한하여서 length 메서드가 가능하게 하였다.
interface Lengthwise { length: number; } function loggingIdentity<Type extends Lengthwise | string>(arg: Type): Type { console.log(arg.length); return arg; }
제네릭 사용 유의사항
제네릭의 장점을 다양한 타입을 받을 수 있어 코드를 효율적으로 재사용할 수 있다는 점이다. 그러다보니 API 응답 값의 타입을 지정할 때 제네릭을 많이 쓰게 된다.
다만 유의할 점은 3가지다
- 제네릭을 굳이 사용하지 않아도 되는 타입
type GType<T> = T;
- any 사용하기
마찬가지로 제네릭과 any를 사용한다면 자바스크립트를 쓰는 것과차이가 없다. - 가독성을 고려하지 않기
너무 남용할 경우 남이 알아보기 힘들다.
'독서' 카테고리의 다른 글
우아한 타입스크립트 with React -6장 타입스크립트 컴파일 (0) | 2024.02.19 |
---|---|
우아한 타입스크립트 with React -5장 타입활용하기 (0) | 2024.02.17 |
우아한 타입스크립트 with React -4장 타입확장하기 * 좁히기 (0) | 2024.02.15 |
우아한 타입스크립트 with React -2장 타입 (0) | 2024.02.13 |
우아한 타입스크립트 with React -1장 들어가며 (0) | 2024.02.11 |