728x90
반응형
※ 인프런 - 타입스크립트 시작하기(by 이재승) 강의를 기반으로 정리한 내용입니다.
제네릭(Generic)
제네릭은 타입 정보가 동적으로 결정되는 타입을 말한다. 제네릭을 통해 같은 규칙을 여러 타입에 적용할 수 있기 때문에 타입 코드를 작성할 때 발생할 수 있는 중복 코드를 제거할 수 있다.
export {};
function makeNumberArray(defaultValue: number, size: number): number[] {
const arr: number[] = [];
for (let i = 0; i < size; i++) arr.push(defaultValue);
return arr;
}
function makeStringArray(defaultValue: string, size: number): string[] {
const arr: string[] = [];
for (let i = 0; i < size; i++) arr.push(defaultValue);
return arr;
}
const arr1 = makeNumberArray(1, 10);
const arr2 = makeStringArray('empty', 10);
console.log(arr1); // [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
console.log(arr2); // ['empty', 'empty', 'empty', 'empty', 'empty', 'empty', 'empty', 'empty', 'empty', 'empty']
// 함수 오버로드. 위의 두 함수를 이처럼 하나로 만들 수 있음
// 필요한 타입 정의
function makeArray(defaultValue: number, size: number): number[];
function makeArray(defaultValue: string, size: number): string[];
// 로직 정의
function makeArray(defaultValue: number | string, size: number): Array<number | string> {
const arr: Array<number | string> = [];
for (let i = 0; i < size; i++) arr.push(defaultValue);
return arr;
}
const arr3 = makeArray(1, 10);
const arr4 = makeArray('empty', 10);
/**
* 하지만 이 방법은 number와 string만 입력 가능함
* 타입을 추가하고 싶으면 타입 정의, 매개변수들의 타입 정의 등 많은 코드 수정해야 하기 때문에 번거로워질 수 있음
* 이를 해결할 수 있는 것이 제네릭!
*/
// 제네릭
function makeArr<T>(defaultValue: T, size: number): T[] {
const arr: T[] = [];
for (let i = 0; i < arr.length; i++) arr.push(defaultValue);
return arr;
}
const arr5 = makeArr<number>(1, 10);
const arr6 = makeArr<string>('empty', 10);
const arr7 = makeArr(1, 10);
const arr8 = makeArr('empty', 10);
/**
* 제네릭은 함수 이름 오른쪽에 <>를 이용해서 입력할 수 있음
* T는 원하는 이름으로 정할 수 있고, 현재 T라는 것의 타입은 정해지지 않음
* T의 타입은 나중에 동적으로 결정될 것
* T는 매개변수 쪽과 구현하는 쪽 모두 사용할 수 있음
* arr5, arr6처럼 함수를 호출할 때 <>를 이용하여 타입 정의
* 하지만 타입스크립트는 유연하기 때문에 arr7, arr8처럼 타입을 따로 정의하지 않아서 자동으로 숫자와 문자열로 정의됨
*/
// 제네릭은 데이터의 타입에 다양성을 부여해주기 때문에 자료구조에서 많이 사용됨
class Stack<D> {
private items: D[] = [];
push(item: D) {
this.items.push(item);
}
pop() {
return this.items.pop();
}
}
const numStack = new Stack<number>();
numStack.push(10);
const v1 = numStack.pop();
const strStack = new Stack<string>();
strStack.push('a');
const v2 = strStack.pop();
let myStack: Stack<number>;
myStack = numStack;
myStack = strStack; // string 형식은 number 형식에 할당 불가능하기 때문에 에러!
// 리액트와 같은 라이브러리의 API는 입력 가능한 값의 범위를 제한함
// 이를 위해 제네릭은 타입의 종류를 제한할 수 있는 기능을 제공
function identity<T extends number | string>(p1: T): T { // A extends B : A가 B에 할당 가능해야 한다
return p1;
}
identity(1);
identity('a');
identity(true); // never[]형식의 인수는 number | string 형식의 매개변수에 할당 불가능
// extends 키워드 자세히 알아보기
interface Person {
name: string;
age: number;
}
interface Korean extends Person {
liveInSeoul: boolean;
}
function swapProperty<T extends Person, K extends keyof Person> (p1: T, p2: T, key: K): void {
const tmp = p1[key];
p1[key] = p2[key];
p2[key] = tmp;
}
/**
* keyof? Person의 모든 속성의 이름을 나열한 것
* type T1 = keyof Person; // type T1 = "name" | "age"
* 즉 K는 name과 age에 할당 가능한 값이어야 함
*/
const p1: Korean = {
name: '기리',
age: 30,
liveInSeoul: true
};
const p2: Korean = {
name: '우니',
age: 29,
liveInSeoul: false
};
swapProperty(p1, p2, 'age'); // 세 번째 매개변수에는 age, name만 나옴. 다른 키 입력 불가능
console.log(p1); // { name: '기리', age: 29, liveInSeoul: true }
console.log(p2); // { name: '우니', age: 30, liveInSeoul: false }
맵드 타입(Mapped Type)
A라는 인터페이스가 있을 때 A의 모든 속성을 readonly로 바꾸거나 optional로 바꾸는 등 이러한 일을 맵드 타입을 통해 할 수 있다.
export {};
// mapped type의 문법
type T1 = { [K in 'prop1' | 'prop2']: boolean};
/**
* 맵드타입으로 만드는 것은 객체타입이기 때문에 중괄호 있고, 그안에 대괄호가 있음
* 대괄호는 key 부분을 나타냄
* 대괄호 안에 in 이라는 키워드를 사용함
* 변수명 K는 아무거나 해도 상관 없음
* in 오른쪽에 있는 키워드들이 전체 객체의 속성으로 만들어짐
* T1에 마우스를 올리면 다음과 같이 나옴
type T1 = {
prop1: boolean;
prop2: boolean;
}
*/
// 인터페이스의 모든 속성을 boolean 타입으로 만들어주는 맵드 타입
interface Person {
name: string;
age: number;
}
type MakeBoolean<T> = { [P in keyof T]?: boolean}; // keyof로 제너릭T의 키들이 유니온 타입으로 만들어짐. ?를 사용했기 때문에 선택 속성
const pMap: MakeBoolean<Person> = {}; // MakeBoolean안에 Person 입력
pMap.name = true; // 원래 string타입인데 boolean으로 바뀜. 선택 속성이기 때문에 undefined도 가능
pMap.age = false; // 원래 number타입인데 boolean으로 바뀜. 선택 속성이기 때문에 undefined도 가능
// readonly 맵드 타입 정의
type T2 = Person['name'];
type Readonly<T> = { readonly [P in keyof T]: T[P] }; // T의 모든 키의 속성에 readonly 붙임
type Partial<T> = { [P in keyof T]?: T[P] };
type T3 = Partial<Person>;
type T4 = Readonly<Person>;
/**
* T[P]? T2처럼 인터페이스에 속성 이름을 적어주면 그 속성의 값의 타입을 의미함. T2는 문자열임
* 즉 T[P]는 각 속성의 원래 값의 타입을 적어준 것. name은 string, age는 number (값의 변화를 주지 않은 것)
* 이렇게 맵드 타입을 이용하면 함수처럼 사용할 수 있는데, 일종의 유틸리티 타입이라고 보면 됨
* 사실 Readonly와 Partial은 타입스크립트에 내장되어 있기 때문에 주석 처리해도 사용 가능
*/
// Pick 타입
type Pick<T, K extends keyof T> = { [P in K]: T[P]}; // Pick도 내장타입
interface Person {
name: string;
age: number;
language: string;
}
type T5 = Pick<Person, 'name' | 'language'>;
// Record 타입. 역시 내장 타입
type Record<K extends string, T> = { [P in K]: T };
type T6 = Record<'p1' | 'p2', Person>; // p1과 p2 속성으로 이루어지고 Person 타입을 값으로 가지는 객체를 만들겠다
// enum 타입 활용도 높이기
enum Fruit {
Apple,
Banana,
Orange
}
const fruitPrice = {
[Fruit.Apple]: 1000,
[Fruit.Banana]: 1500,
[Fruit.Orange]: 2000
};
// 만약 Fruit enum에 새로운 과일을 추가하게 되면 fruitPrice 객체에 새로 추가된 과일 속성을 추가해야 하는 번거로움이 있음
const fruitPrice2: { [key in Fruit]: number} = {
[Fruit.Apple]: 1000,
[Fruit.Banana]: 1500,
[Fruit.Orange]: 2000,
};
// 객체를 맵드 타입으로 활용하면 enum의 모든 속성을 적지 않으면 에러가 나기 때문에 실수로 누락해도 바로 정정할 수 있음
조건부 타입(Conditionnal Type)
/** 조건부 타입 (Conditional Type)
* 조건부 타입: 입력된 제네릭 타입에 따라 타입을 결정할 수 있는 기능
* 조건부 타입의 문법: T extends U ? X : Y
* 제네릭으로 입력된 어떤 타입 T가 U에 할당 가능하다면 X, 아니면 Y 타입 사용
*/
export {};
type IsStringType<T> = T extends string ? 'yes' : 'no';
type T1 = IsStringType<string>; // T1 = "yes"
type T2 = IsStringType<number>; // T2 = "no"
/**
* T가 문자열로 할당이 가능하다면 yes라는 문자열 리터럴 타입 사용
* 아니라면 no 타입 사용
* 주의할 점: 자바스크립트의 삼항연산자와 비슷해보이지만 다름!
* 조건부 타입은 값을 다루는 것이 아니라 타입을 다루는 것임
*/
// 조건부 타입은 유니온 타입과 자주 사용됨
type T3 = IsStringType<string | number>; // T3 = "yes" | "no"
type T4 = IsStringType<string> | IsStringType<number>; // T4 = "yes" | "no"
/**
* 조건부 타입은 유니온 타입과 같이 사용되면 T3, T4처럼 독특하게 사용 가능
* 단, 조건부 타입 + 유니온 타입일 때만 이런 결과 나옴!
*/
type Array2<T> = Array<T>;
type T5 = Array2<string | number>;
/**
* T5는 T3, T4처럼 조건부 타입을 쓰지 않았기 때문에
* string[] | number[] 이런 타입이 만들어지지 않음
* 조건부 타입이 아니기 때문에 T5는 문자열 또는 숫자의 배열이 됨. (string | number)[]
*/
// Exclude, Extract
type T6 = number | string | never; // 유니온 타입에 있는 never는 해당되는 속성을 제외시킴
// Exclude: T가 U에 할당 가능한 타입 제거
type Exclude<T, U> = T extends U ? never : T;
type T7 = Exclude<1 | 3| 5 | 7, 1| 5| 9>; // T7 = 3 | 7
type T8 = Exclude<string | number | (() => void), Function>; // Function에 해당되는 것은 제거. T8 = string | number
// Extract: T가 U에 할당 가능하지 않으면 타입 제거
type Extract<T, U> = T extends U ? T : never;
type T9 = Extract<1 | 3 | 5 | 7, 1 | 5 | 9>; // T9 = 1 | 5
// Return Type: T가 함수일 때 T 함수의 반환 타입을 뽑아줌
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
/**
* 함수 T가 extends 뒤의 함수에 할당 가능한 타입이면 R이라는 것을 사용
* R: 이 함수의 반환 타입 의미
* 타입 추론을 위해 infer라는 키워드 사용
* infer 키워드는 조건부 타입을 사용할 때 extends 키워드 뒤에서 이렇게 사용됨
*/
function f1(s: string): number {
return s.length;
}
type T10 = ReturnType<typeof f1>; // T10 = number;
/**
* 리턴타입은 제네릭에 함수만 입력할 수 있기 때문에 값으로부터 타입을 가져오기 위해서 typeof 키워드 사용함
*/
// infer 키워드 자세히 알아보기
type Unpacked<T> = T extends (infer U) [] // T 타입이 어떤 값의 배열이면(어떤 배열인지는 아직 정해지지 않았기 때문에 infer)
? U // U 배열의 타입을 사용하겠다
: T extends (...args: any[]) => infer U // 배열이 아닐 시, 함수에 할당 가능한 타입이라면
? U // 함수의 반환 타입을 사용하겠다
: T extends Promise<infer U> // 아니라면 프로미스에 할당 가능한 타입이라면(프로미스의 어떤 값인지는 아직 결정X --> infer)
? U // 프로미스의 값인 U를 사용하겠다
: T; // 모두 만족하지 않으면 자기 자신인 T 사용
type T11 = Unpacked<string>;
type T12 = Unpacked<string[]>;
type T13 = Unpacked<() => string>;
type T14 = Unpacked<Promise<string>>;
type T15 = Unpacked<Promise<string>[]>;
type T16 = Unpacked<Unpacked<Promise<string>[]>>;
// 조건부 타입 사용하여 몇 가지 유틸리티 타입 만들어보기
// 인터페이스에서 값이 문자열인 속성 이름을 추출하는 유틸리티 타입
type StringPropertyNames<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
interface Person {
name: string;
age: number;
nation: string;
}
type T17 = StringPropertyNames<Person>;
type StringProperties<T> = Pick<T, StringPropertyNames<T>>;
type T18 = StringProperties<Person>; // Pick을 통해 스트링 타입의 속성만 모아놓은 객체 생성
// Omit
type Omit<T, U extends keyof T> = Pick<T, Exclude<keyof T, U>>;
type T19 = Omit<Person, 'nation' | 'age'>; // Person인터페이스에서 nation과 age를 제거한다 는 의미
// Overwrite
/**
* 타입스크립트에 내장된 타입은 아님
* 두 인터페이스를 받아서 T라는 인터페이스를 베이스로 해서 U를 T로 덮어쓰겠다!
*/
type Overwrite<T, U> = { [P in Exclude<keyof T, keyof U>]: T[P] } & U; // T와 U 중에서 겹치는 것은 Exclude로 제거, 그후 U와 교집합
interface Person {
name: string;
age: number;
}
type T20 = Overwrite<Person, {age: string; nation: string}>;
const p: T20 = {
name: 'mike',
age: '23',
nation: 'Korea'
};
728x90
반응형
'개발자 도전기 > [STUDY] JS || TS' 카테고리의 다른 글
TypeScript | 강의 메모 | CLI에서 동작하는 todo앱 프로젝트 (0) | 2022.04.13 |
---|---|
TypeScript | 강의 메모 | 타입 추론, 타입가드 (0) | 2022.04.05 |
TypeScript | 강의 메모 | 타입 호환성 (0) | 2022.04.01 |
TypeScript | 강의 메모 | 타입 정의 (0) | 2022.03.31 |
TypeScript | 강의 메모 | 타입스크립트란? (0) | 2022.03.30 |
댓글