[TS] Section7 제네릭

lim1313·2022년 2월 8일
0

TIL

목록 보기
14/22

🍊 제네릭

제네릭은 어떠한 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 프로그래밍 기법을 말한다.

제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다. 한번의 선언으로 다양한 타입에 재사용이 가능하다는 장점이 있다.

🍉 제네릭 함수 생성하기

예시 1

다양한 타입을 가지는 경우 아래와 같이 나열하여 사용할 수도 있다.

function getSize(arr: number[] | string[] | boolean[]): number {
  return arr.length;
}

const arr1 = [1, 2, 3];
getSize(arr1);

const arr3 = [true, false, true];
getSize(arr3);

하지만, 길이가 길어지는 등의 문제가 있기 때문에, 간단하게 제네릭을 이용하여 다양한 타입을 적용할 수 있다. 생성하는 시점에 타입을 지정해 준다.

  function getSize<T>(arr: T[]): number {
    return arr.length;
  }

  const arr1 = [1, 2, 3];
  getSize<number>(arr1);

// 전달되는 매개변수를 통해 타입스크립트는 어떤 타입인지 추론하므로
// 타입을 지정해 주지 않아도 동작한다.
  const arr3 = [true, false, true];
  getSize(arr3);

예시 2

아래의 예시에서 mergeObj.name 은 에러가 발생한다.
(타입스크립트는 name이 있는지 없는지 알지 못하기 때문이다.)

function merge(objA: object, ojbB: object) {
  return Object.assign(objA, ojbB);
}

const mergeObj = merge({ name: 'max' }, { age: 30 });
console.log(mergeObj.name); //=> Error 

아래와 같이 제네릭을 통해 표현할 수 있다.
정확히 어떤 타입이 될지 모른다는 추가 정보를 타입스크립트에 제공한 것이다.

function merge<T, U>(objA: T, ojbB: U) {
  return Object.assign(objA, ojbB);
}

const mergeObj = merge({ name: 'max' }, { age: 30 });
console.log(mergeObj.name);

// 아래와 같이 직접 타입을 지정해줄 수 있다.
const mergeObj = merge<{name:string}, {age:number}>({ name: 'max' }, { age: 30 });

interface에서의 제네릭

interface Mobile<T> {
  name: string;
  price: number;
  option: T;
}

const m1: Mobile<object> = {
  name: 's21',
  price: 1000,
  option: {
    color: 'red',
    coupon: false,
  },
};

const m3: Mobile<{ color: string; coupon: boolean }> = {
  name: 's21',
  price: 1000,
  option: {
    color: 'red',
    coupon: false,
  },
};

const m2: Mobile<string> = {
  name: 's20',
  price: 1000,
  option: 'good',
};

🍉 제약 조건 작업하기 extends

예시 1

아래의 코드에서 Object.assign은 객체를 병합하는 메소드이지만, objB에 number를 할당하여도 에러가 발생하지 않는다.

function merge<T, U>(objA: T, ojbB: U) {
  return Object.assign(objA, objB);
}

const mergeObj = merge( { name: 'max' }, 30 );

console.log(mergeObj.name);

이와 같이 제약 조건이 있는 경우, 즉 objB가 object 타입이어야만 하는 경우 extends를 통해 제약을 걸 수 있다.

function merge<T extends object, U extends object>(objA: T, ojbB: U) {
  return Object.assign(objA, ojbB);
}

const mergeObj = merge({ name: 'max' }, 30); //=> Error
console.log(mergeObj.name);

예시 2

  interface User {
    name: string;
    age: number;
  }

  interface Book {
    price: number;
  }

  const user: User = { name: 'a', age: 10 };
  const book: Book = { price: 1000 };

  function showName<T extends { name:string }>(data: T): string {
    return data.name;
  }

  showName(user);
  showName(book); //=> Error (name이 없기 때문)

예시 3

// 문자열이든 배열이든 상관없이,
// length 속성을 지니는지만 신경쓴다.

interface Lengthy {
  length: number;
}

function countAndDescribe<T extends Lengthy>(element: T): [T, string] {
  let descriptionText = 'Got no value.';
  if (element.length === 1) {
    descriptionText = `Got 1 element.`;
  } else if (element.length > 1) {
    descriptionText = `Got ${element.length} elements`;
  }
  return [element, descriptionText];
}

console.log(countAndDescribe('hi there '));
console.log(countAndDescribe(['a','b'));
console.log(countAndDescribe(123)); //=> Error

keyof 제약 조건

아래와 같이 입력하면 입력한 객체가 무엇이든 key를 가지는지 알 수 없기 때문에 에러가 발생한다.
즉, 타입스크립트가 obj 객체가 key라는 키를 가지고 있는지 보장할 수 없다.

function extractAndConvert(obj: object, key: string){
  return obj[key] // => Error
}

이를 보장하기 위해서 keyof를 활용한 제네릭을 사용하면 된다.
타입스크립트에게 첫 번째 매개변수가 모든 유형의 객체여야 하고, 두번째 매개변수는 해당 객체의 모든 유형의 키여야 한다고 입력했기 때문이다.

function extractAndConvert<T extends object, U extends keyof T>(obj: T, key: U){
  return obj[key]
}

extractAndConvert({}, 'name'); // => Error
extractAndConvert({ name: 'max'}, 'age'); // => Error
extractAndConvert({ name: 'max' }, 'name');

🍉 제네릭 클래스

class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data.splice(this.data.indexOf(item), 1);
  }

  getItems() {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
// textStorage.addItem(10); //=> Error
textStorage.addItem('one');

const numberStorage = new DataStorage<number>();
numberStorage.addItem(12);

위의 코드를 보면 T에는 어떤 타입이든 올 수 있다.
하지만, removeItem의 경우 object 타입이 오게 되면 우리가 의도하지 않게 동작할 수 있다.(객체는 참조타입이기 때문)

때문에 좀 더 구체적으로 허용하는 타입을 제한해 줄 필요가 있다.

class DataStorage<T extends string | number | boolean> {
  ...
}
  
//  const objStorage = new DataStorage<object>(); //=> Error

이렇게 되면 더이상 object 타입이 유효하지 않게 된다.


🍉 제네릭 유틸리티 타입

Partical

파셜 타입은 특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있다.

TYPE의 모든 속성을 선택적으로 변경한 새로운 타입 반환 (인터페이스)

interface Address {
  email: string;
  address: string;
}

type MyEmail = Partial<Address>;
const me: MyEmail = {}; // 가능
const you: MyEmail = { email: "noh5524@gmail.com" }; // 가능
const all: MyEmail = { email: "noh5524@gmail.com", address: "secho" }; // 가능
interface CourseGoal {
  title: string;
  description: string;
  completeUntil: Date;
}

function createCourseGoal(
  title: string,
  description: string,
  date: Date
): CourseGoal {
  let courseGoal: Partial<CourseGoal> = {};

  courseGoal.title = title;
  courseGoal.description = description;
  courseGoal.completeUntil = date;

  // return courseGoal;
  //=> Error (courseGoal이 일반 CourseGold 타입이 아닌 CourseGold의 partial 타입이기 때문이다.)

  // 때문에 CourseGoal로 형 변환하여 해결할 수 있다.
  return courseGoal as CourseGoal;
}

Required<T>

TYPE의 모든 속성을 필수로 변경한 새로운 타입 반환 (인터페이스)

interface IUser {
  name?: string,
  age?: number
}

const userA: IUser = {
  name: 'A'
};
const userB: Required<IUser> = { // TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'Required<IUser>'.
  name: 'B'
};

Readonly<T>

T의 모든 프로퍼티를 읽기 전용(readOnly)으로 설정한 타입을 구성한다. 즉 모든 프로퍼티의 값을 변경할 수 없고 참조만 할 수 있도록 만든다.

const names: Readonly<string[]> = ['max', 'sports'];

// names.push('manu'); //=> Error
interface IUser {
  name: string,
  age: number
}

const userA: IUser = {
  name: 'A',
  age: 12
};
userA.name = 'AA';

const userB: Readonly<IUser> = {
  name: 'B',
  age: 13
};
userB.name = 'BB'; // TS2540: Cannot assign to 'name' because it is a read-only property.

위 예제의 Readonly는 다음과 같이 이해할 수 있다.

interface INewType {
  readonly name: string,
  readonly age: number
}

Record<T>

KEY를 속성으로, TYPE를 그 속성값의 타입으로 지정하는 새로운 타입 반환 (인터페이스)

const developers: Record<string, number> = {
  apples: 10,
  oranges: 20
}
type TName = 'neo' | 'lewis';
const developers: Record<TName, number> = {
  neo: 12,
  lewis: 13
};
interface Starship {
  name: string;
  enableHyperjump: boolean;
}

const starships: Record<string, Starship> = {
  Exploerer1: {
    name: 'Explorer1',
    enableHyperjump: true,
  },
  Exploerer2: {
    name: 'Explorer2',
    enableHyperjump: false,
  },
};

Pick<T>

interface IUser {
  name: string,
  age: number,
  email: string,
  isValid: boolean
}
  
type TKey = 'name' | 'email';

const user: Pick<IUser, TKey> = {
  name: 'Neo',
  email: 'thesecon@gmail.com',
  age: 22 // TS2322: Type '{ name: string; email: string; age: number; }' is not assignable to type 'Pick<IUser, TKey>'.
};

Omit<T>

Pick과 반대이다.
TYPE에서 KEY로 속성을 생략하고 나머지를 선택한 새로운 타입을 반환한다. TYPE은 속성을 가지는 인터페이스나 객체 타입이어야 한다.

interface IUser {
  name: string,
  age: number,
  email: string,
  isValid: boolean
}
type TKey = 'name' | 'email';

const user: Omit<IUser, TKey> = {
  age: 22,
  isValid: true,
  name: 'Neo' // TS2322: Type '{ age: number; isValid: true; name: string; }' is not assignable to type 'Pick<IUser, "age" | "isValid">'.
};

Exclude<T>

유니언 TYPE1에서 유니언 TYPE2를 제외한 새로운 타입을 반환한다.

type T = string | number;

const a: Exclude<T, number> = 'Only string';
const b: Exclude<T, number> = 1234; // TS2322: Type '123' is not assignable to type 'string'.
const c: T = 'String';
const d: T = 1234;

Extract<T>

유니언 TYPE1에서 유니언 TYPE2를 추출한 새로운 타입을 반환한다.

type T = string | number;
type U = number | boolean;

const a: Extract<T, U> = 123;
const b: Extract<T, U> = 'Only number'; // TS2322: Type '"Only number"' is not assignable to type 'number'.

NonNullable<T>

유니언 TYPE에서 null과 undefined를 제외한 새로운 타입을 반환한다.

type T = string | number | undefined;

const a: T = undefined;
const b: NonNullable<T> = null; // TS2322: Type 'null' is not assignable to type 'string | number'.
interface StarshipProperties {
  color?: 'blue' | 'red' | 'green';
}

function paintStarship(
  id: number,
  color: NonNullable<StarshipProperties['color']>
) {}

paintStarship(1, 'blue');
paintStarship(1, undefined); // Erorr

ReturnType<T>

함수 TYPE의 반환(Return) 타입을 새로운 타입으로 반환한다.

  function fn(str: string) {
  return str;
}

const a: ReturnType<typeof fn> = 'Only string'; // string type
const b: ReturnType<typeof fn> = 1234; // TS2322: Type '123' is not assignable to type 'string'.

Omit<T>

Omit<T>

Omit<T>

Omit<T>

Omit<T>

profile
start coding

0개의 댓글