Front-End/TypeScript

제네릭 & 타입 가드

Voyage_dev 2024. 7. 24. 23:29

제네릭?

  • 타입을 마치 클래스나 함수 등에서 파라미터처럼 사용하는 것을 의미한다.
class ArrayOfNumbers {
	constructor(public collection: number[]) {}
	
	get(index: number): number {
		return this.collection[index];
	}
}

class ArrayOfStrings {
	constructor(public collection: string[]) {}
	
	get(index: number): string {
		return this.collection[index];
	}
}

number 타입, string 타입인거를 제외하면 나머지 부분은 전반적으로 동일하다. number 배열, string 배열을 생성자에서 매개변수로 받고 그 중에서 특정 인덱스의 값을 리턴하는 코드다.

 

타입 빼고 나머지 코드는 동일하다 즉, 중복되는 부분이 굉장히 많은데 이거를 하나의 클래스로 만들 수는 없을까?

 

이 두 개의 코드에서 중복되는 부분을 하나로 묶어서 하나의 클래스로 만들어 보자

class ArrayOfAnything<T> {
	constructor(public collection: T[]) {}
	
	get(index: number): T {
		return this.collection[index];
	}
}

new ArrayOfAnything<string>(['a', 'b', 'c']) 
  • 어떤 타입이 들어올지 모르기 때문에 <T> 이렇게 타입이 들어온다고 하고 collection 타입 어노테이션과 get 함수에 각각 T로 만들어주므로 T가 number일 때는 number 배열, number 리턴, string일 때는 string 배열 string 리턴으로 위에서 작성했던 두 가지의 역할을 동일하게 할 수 있다.

⇒ 클래스에서 제네릭을 통해서 중복되는 내용들이 많은 클래스를 하나로 합치는 코드로 만들수 있다.

 

그럼 함수는?

function printStrings(arr: string[]): void {
        for (let i = 0; i < arr.length; i++) {
                console.log(arr[i]);
        }
}

function printNumbers(arr: number[]): void {
        for (let i = 0; i < arr.length; i++) {
                console.log(arr[i]);
        }
}

**function printAnything<T>(arr: T[]): void {
        for (let i = 0; i < arr.length; i++) {
                console.log(arr[i]);
        }
}**

printAnything(['a', 'b', 'c']); // <string>을 써주지 않아도 타입 추론이 가능
printAnything([1,2,3]); // number 배열도 받을 수 있다
 
  • 자동적으로 타입을 추론하기 때문에 따로 타입을 적어주지 않아도 문제없이 돌아간다.

하지만 제네릭을 마음껏 사용하면 될까?

  • 규칙을 제한할 수 있다
class Car {
	print() {
		console.log('car');
	}
}

class House {
	print() {
		console.log('house');
	}
}

function printHousesOrCars<T>(arr: T[]): void {
	for (let i = 0; i < arr.length; i++) {
		arr[i].print();
	}
}

printHousesOrCars([1,2,3]);
  • 이 제네릭이 print를 가질 수 있게 제약조건을 만들어 줘야 한다
  • print 메서드가 없으면 에러를 반환한다
class Car {
	print() {
		console.log('car');
	}
}

class House {
	print() {
		console.log('house');
	}
}

interface Printable {
	print(): void
}

function printHousesOrCars<T extends Printable>(arr: T[]): void {
	for (let i = 0; i < arr.length; i++) {
		arr[i].print();
	}
}

printHousesOrCars([1,2,3]); // error
printHousesOrCars<House>([new House(), new House()])

타입가드?

타입스크립트로 개발 하다보면 때로는 굉장히 타입이 넓게 정의가 되어서 타입스크립트의 유효성을 올바르게 가져가지 못 하는 경우가 많다. 그래서 이러한 문제들을 해결하기 위해서 사용하는 개념이 타입 가드이다.

  • 타입 가드는 조건문에서 객체의 타입을 좁혀 나갈 수 있는 방법이다.
  • 타입을 분기처리를 통해서 정확한 타입을 찾고 그 안에서 정확한 로직을 만들어야 나중에 사이드 이펙트나 버그가 줄어들 수 있게 된다.
  • 타입 가드에는 굉장히 많은 문법들이 있다.

typeof

 

우리가 유니언 타입으로 인자를 받는다고 한 번 가정해 보자

function doSomething(x: number | string) {
	if (typeof x === 'string') {
		console.log(x.substr(1)); // string에는 substr 메서드가 존재하지만, number에는 존재하지 않습니다.
	}
}
  • 인자를 number와 string 타입으로 받고 있는데 만약에 여기에서 x의 substr 을 구하고 싶을 때 substr는 string 타입에만 가지고 있기 때문에 number에는 없다.
  • 타입스크립트에서는 이 x가 number인지 string인지 모르기 때문에 에러를 뱉고 있다.
  • 이러한 경우에 타입 가드를 통해서 조건문을 분기처리 할 수 있다.

instanceof

  • 타입을 위에처럼 찾는 게 아니라 어떤 클래스를 인스턴스로 만들어 쓰는 경우에 사용
class Foo {
  foo = 123;
  common = '123';
}

class Bar {
  bar = 123;
  common = '123';
}

function doStuff(arg: Foo | Bar) {
  if (arg instanceof Foo) {
    console.log(arg.foo)
  }
  if (arg instanceof Bar) {
    console.log(arg.bar);
  }

  console.log(arg.common); 
  console.log(arg.foo); // Error
  console.log(arg.bar); // Error
}
  • 위에 두 가지 필드를 가지고 있는 클래스로 예를 들어보자.
  • arg 인자가 Foo 혹은 Bar 클래스를 가지고 있을 때 둘 다 common 속성을 가지고는 있지만 둘 다 foo나 bar를 가지고 있지 않기 때문에 에러를 뱉는다.
  • 이럴 때는 인자가 instanceof Foo 혹은 Bar라면 이라는 타입 가드를 통해서 에러를 발생하지 않게 할 수 있다.

in

  • 객체 안에서 어떤 프로퍼티를 가지고 있는지 파악하는 문법이다.
interface A {
  x: number;
}
interface B {
  y: string;
}

function doStuff(q: A | B) {
  if ('x' in q) {
  // q: A
  }
  else {
  // q: B
  }
}
  • 각각의 타입별로 분기 처리할 수 있다.