[TIL] Typescript 제네릭 및 유틸리티 타입

seo young park·2022년 8월 28일
5

TIL

목록 보기
7/7
post-thumbnail

자바스크립트는 동적 타입 언어라 변수의 타입이 런타임 시 결정되고, 타입 에러 또한 런타임에 발견된다. 그래서 자바스크립트에 정적 타입 문법을 추가한 타입스크립트를 사용하여, 코드 작성 단계에서 타입 에러를 사전에 제거하게 된다.
타입스크립트 개념들을 다시 정리해보고자 한다.

제네릭

제네릭이란

인수를 그대로 반환하는 foo라는 함수가 있다.

function testFn(args: any): any {
    return args;
}

testFn(2); // number 타입을 넘겨도 any 타입이 반환된다는 정보만 있음
function testFn<T>(args: T): T {
    return args;
}

testFn(2); // 인수와 반환 타입이 같음. number 타입을 넘기면 number 타입이 반환된다는 정보가 있음

제네릭 타입과 any의 공통점은, 어떤 타입도 받을 수 있다는 것이고
차이점은, 제네릭은 함수의 반환 타입 정보를 알고 any는 모른다는 것이다.

  • 제네릭 함수 호출방법
  1. 타입 인수를 포함한 모든 인수를 넘김

명시적으로 타입 인수 전달

const result = testFn<string>('test'); // string 타입 반환
                          
  1. 타입 인수 추론

컴파일러가 전달하는 인수에 따라 타입 결정

const result = testFn('test'); // string 타입 반환

any 대신 제네릭을 사용하는 이유

    function genericFn<T>(args: T): T {
      console.log(args.length);
      //Property 'length' does not exist on type 'T'
      return text;
    };

    function anyFn(args:any):any {
      console.log(args.length);
    }

제네릭과 any의 공통점은 여러 가지 타입을 허용한다는 것이고,
차이점은 제네립은 반환 타입의 정보를 알고 있고 any는 모른다는 점이다. 인자의 length를 확인하는 코드를 작성하면, 제네릭을 사용한 함수는 length 가 없다는 에러를 뱉는다. 이 때, 여러가지 타입의 배열 형태를 받도록 설정할 수 있다.

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

제네릭 인터페이스

  • 제네릭 인터페이스
interface GenericTestFn {
  <T>(arg: T): T;
}

function firstTestFn<T>(arg: T): T {
  return arg;
}

let secondFn: GenericTestFn = firstTestFn;
  • 특정한 타입 인수를 받는 제네릭 인터페이스
interface GenericTestFn<T> {
  (arg: T): T;
}

function firstTestFn<T>(arg: T): T {
  return arg;
}

let secondFn: GenericTestFn<number> = firstTestFn;

firstTestFn('abc');
firstTestFn(123);
secondFn('abc'); // Argument of type 'string' is not assignable to parameter of type 'number'.
secondFn(123);

제네릭 제약조건

특정 타입들에만 동작하는 제네릭 함수를 만들 수 있다. 예를 들어 length 프로퍼티가 있는 타입만 받도록 제한해보자.

제약 조건이 있는 인터페이스를 만든다. 그리고 extend 키워드를 이용해 제약 조건을 명시한다.

    interface LengthWise {
      length: number;
    }
    
    function testFn<T extends LengthWise>(args: T): T {
      return args;
    }

    const withLenghObject = {
      length: 2
    }

    const withoutLengthObject = {
      age: 3
    }


    testFn([1,2,3]);
    testFn(['a','b','c']);
    testFn(123); // Argument of type 'number' is not assignable to parameter of type 'LengthWise'.
    testFn(withLenghObject);
    testFn(withoutLengthObject); //  Property 'length' is missing in type '{ age: number; }' but required in type 'LengthWise'.

제네릭 제약조건에 타입 매개 변수 사용

객체와 객체의 key를 받으면 이에 해당하는 프로퍼티를 반환하는 함수가 있다. 객체에 존재하지 않는 프로퍼티를 인수로 넘기지 않도록 제약 조건을 걸 수 있다.

    function getProperty<T, K extends keyof T>(obj: T, key:K) {
      return obj[key];  
    }
    let obj = { age: 23 };
    
    getProperty(obj, "age"); 
    getProperty(obj, "address"); // error, Argument of type '"address"' is not assignable to parameter of type '"age"'.

유틸리티 타입

유틸리티 타입이란, 이미 정의한 타입을 변환할 때 사용하는 타입 문법이다. 자주 사용되는 문법들을 알아보자.

Partial< T >

특정 타입의 부분집합을 나타내는 타입이다.

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

function updateUser(user: User, fieldUpdate: Partial<User>) {
    return { ...user, ...fieldUpdate };
}

const user1 = {
    name: 'yujin',
    email: 'yujin1999@naver.com',
  	age: 29,
};

const user2 = updateUser(user, {
    email: 'dev-yujin@google.com',
});

Readonly < T >

T의 모든 프로퍼티를 읽기 전용으로 설정하며, 해당 프로퍼티는 재할당할 수 없다.


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

const user: Readonly<User> = {
   name: 'yujin'
   email: 'yujin@google.com',
   age: 29,
};

user.name = 'mimi'; // Cannot assign to 'name' because it is a read-only property.

Record <K,T>

<Key, Type>으로 Key(key):Type(value) 형태의 타입이다.

interface Person {
	age: number
}

type Member = 'mimi' | 'yujin' | 'youngji'

const example: Record<Member, Person> = {
	'mimi': {age: 25},
  	'yujin': {age: 27},
  	'youngji': {age: 29},
}

기존 타입의 key를 활용하는 경우에도 적용 가능하다.

type UserType = {
  name: string;
  age: number;
  email: string;
};

type UserRecordType = Record<keyof UserType, string>;

let user: UserType = {
  name: 'yujin',
  age: "29",
  email: "dev-yujin@gmail.com"
};

Record vs Index Signature

인덱스 시그니처(Index Signature)란, {[Key: T] : U} 형태의 타입으로, key와 value의 타입을 정확하게 명시할 때 사용한다.

type User = {
  [key:string]: string | number
}

const user: User {
  'name': 'yujin',
  'age' : 29,  
}

그러나 다음과 같은 문제점들 때문에 인덱스 시그니쳐를 지양하는데

  1. key 타입은 string, number, symbol, Template literal 만 가능하다.
type User = {
  [key: string | boolean]: string; // An index signature parameter type must be 'string', 'number', 'symbol', or a template literal type.
}
  1. 존재하지 않는 속성에 접근 가능하다.
type User = {
  [key: string]: string;
}

const user: User = {
  name: 'yujin',
};

console.log(user.address); // undefined;
  1. 빈 객체도 할당할 수 있다.
type User = {
  [key: string]: string; 
}

const user: User = {};
  1. 모든 키가 같은 타입을 가져야한다.

따라서 객체의 키를 알 수 있는 상황이라면 Record타입을 사용하는 것이 좋다.

Pick< T >

특정 타입에서 몇 개의 속성을 선택한 타입이다.

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

type AnonymousUserType = Pick<User, 'email' | 'phone'>;

const user: AnonymousUserType = {
    email: 'dev-yujin@google.com',
  	age: 29,
};

Omit< T, K >

T에 대한 K의 차집합(T-K)을 나타내는 타입이다.

예를 들어 User 타입 중 game은 제외하고 email과 password만 필요하다고 가정해보자.

interface User {
  	email: string;
  	password: string;
  	game: {
    	kill: number;
  		death: number;
      	assist: number;
    }
}

const userOne: Omit<User, 'game'> = {
	email: 'game123@google.com',
    password: '1234',
};

참고
https://typescript-kr.github.io/pages/generics.html
https://typescript-kr.github.io/pages/utility-types.html

0개의 댓글