TypeScript 간단 정리 - 4
제네릭
제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것
function getText(text: any): any {
return text;
}
getText('hi'); // 'hi'
getText(10); // 10
getText(true); // true
위 함수는 text라는 파라미터에 값을 넘겨 받아 text를 반환해준다. 파라미터인 text의 타입이 any 이기떄문에 함수의 반환 값 또한 any가 될 수 있다.
하지만 그 의미는 우리가 의도한것과 살짝 다르다. 왜냐하면 text에 string 자료형이 들어오면 함수의 return 값의 자료형은 string이 되어야하는데 반환 값의 타입이 any 여서 어떤 자료형이든 반환이 가능한 상태이기 때문이다.
우리는 string 자료형이 들어오면 반환 값 타입을 string 이라고 알려주고 싶고 숫자가 들어오면 숫자가 반환될 타입이다 라고 알려주고 싶다.
이럴 때 사용할 수 있는 게 제네릭이다.
function getText<T>(text: T): T {
return text;
}
getText<string>('hi');
getText<number>(10);
getText<boolean>(true);
위 함수가 제네릭 기본 문법이 적용된 형태이다. 제네릭을 적용하여 함수를 호출할 때 함수 안에서 사용할 타입을 넘겨줄 수 있다. (<>로 타입은 적는다)
getText<string>('hi’) 는 getText('hi’) 로 <string> 를 생략할 수 있다.
‘hi' 라는 문자열을 타입이 T인 text param에 넣어주기 때문에 T = string 되면서 생략이 가능한 것이다.
제네릭은 보통 여러타입을 넣을 수 있는 공통 함수에서 많이 쓰인다.
추가 예시
function getItemArray(arr: any[], index: number): any {
return arr[index];
}
function pushItemArray(arr: any[], item: any): void {
arr.push(item);
}
const techStack = ['js', 'react'];
const nums = [1, 2, 3, 4];
getItemArray(techStack, 0); // 'js'
pushItemArray(techStack, 'ts'); // ['js', 'react', 'ts']
getItemArray(nums, 0); // 1
pushItemArray(nums, 5); // [1, 2, 3, 4, 5];
제네릭 적용
function getItemArray<T>(arr: T[], index: number): T {
return arr[index];
}
function pushItemArray<T>(arr: T[], item: T): void {
arr.push(item);
}
const techStack = ['js', 'react'];
const nums = [1, 2, 3, 4];
getItemArray(techStack, 0); // 'js'
pushItemArray<string>(techStack, 'ts'); // ['js', 'react', 'ts']
// pushItemArray<number>(techStack, 123); // Error
getItemArray(nums, 0); // 1
pushItemArray(nums, 5); // [1, 2, 3, 4, 5];
제네릭 타입 변수
function printOut<T>(input: T): T {
console.log(input.length); // Error: T doesn't have .length
return input;
}
위 코드를 보면 컴파일러에서 에러를 발생시킨다. 왜냐하면 input에 .length가 있다는 단서는 어디에도 없기 때문이다. 우리는 input에 string이나 array를 넣으면 length가 올바르게 출력이 되고, input에 number를 넣으면 오류가 날 것을 알지만 TS 입장에서는 어떤 타입이 들어올지 알 수 없기 때문에 에러를 띄우고 있는 것이다.
이러한 경우에는 제네릭에 타입을 줄 수 있다.
function printOut<T>(input: T[]): T[] {
console.log(input.length);
return input;
}
printOut([1, 2, 3]);
바로, T 뒤에 배열을 나타내는 대괄호 []를 붙여주는 것이다. 이렇게 해주면 인자 값으로는 배열 형태의 T를 받는다는 의미가 된다. 예를 들면, 함수에 [1,2,3] 처럼 숫자로 이뤄진 배열을 받으면 반환 값으로 number 배열를 돌려주는 것이 있다. 배열인 경우는 length가 무조건 있으니 당연히 에러가 발생하지 않을 것이다.
제네릭 제약 조건
function printOut<T>(input: T): T {
console.log(input.length); // Error: T doesn't have .length
return input;
}
타입 변수랑 동일한 예시인데 에러 해결을 타입 변수가 아닌 제약 조건으로 해결해보겠다.
interface LengthWise {
length: number;
}
function printOut<T extends LengthWise>(input: T): T {
console.log(input.length);
return input;
}
// printOut(10); // Error, 숫자 타입에는 `length`가 존재하지 않으므로 오류 발생
// printOut({ length: 0, value: 'hi' }); // `input.length` 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음
인터페이스 확장을 활용하여 문제를 해결했다.
타입 추론
정적 타입언어를 사용할 떄 단점은 바로 타입을 정의하는데 시간과 노력이 많이 들기 때문에 생산성이 저하될 수도 있다는 것이다.
Typescript는 다양한 경우에 대해 타입 추론을 제공해주기 때문에 여러분이 꼭 필요한 경우에만 타입 정의를 할 수 있다.
기본 타입 추론
let a = 123;
let b = 'abc';
a = 'abc';
b = 123;
위 코드에서는 type를 전혀 작성하지 않았다. 하지만 let 으로 선언한 a, b 에 각각 number와 string 을 넣는 경우에 IDE 상에서 해당 변수에 마우스를 올려보면 a 는 number, b 는 string 자동으로 타입을 추론 해준다.
그래서 a 는 string을 못 넣고 b 는 number를 못 넣는 것이다.
이렇게 타입을 명시하지 않아도 자동으로 타입을 넣어주는 것을 타입 추론이라고 한다.
const c1 = 123;
const c2 = 'abc';
const arr = [1, 2, 3];
const [n1, n2, n3] = arr;
arr.push('a'); // Error
let 은 재할당이 가능해서 어느정도 융통성있게 타입추론이 되지만 const 로 선언한 상수는 재할당이 불가능하기 때문에 let 변수보다 엄격하게 타입이 결정된다.
위 코드에서 c1 에 123를 넣었는데 이 경우는 타입이 number 가 아니라 123이다. 마찬가지로 c2 는 ‘abc’이다.
TS는 number 또는 string 의 범위를 훨씬 좁혀서 c1, c2 의 타입을 추론한 것이다.
arr 을 보면 숫자 배열을 할당해서 자동으로 number 배열을 타입으로 TS가 추론했다. destructuring을 한 경우에도 n1, n2, n3 다 number가 되었다.
마지막으로 arr 에 push를 한 경우에는 arr 의 타입이 number[] 이기 때문에 문자열은 push 할 수 없다고 경고메세지를 보여준다.
const obj = { numId: 1, stringId: '1' };
const { numId, stringId } = obj;
console.log(numId === stringId);
object의 경우에도 비슷하다.
numId, stringId 각각 할당한 obj 에 마우스를 올려보면 obj 의 타입이 numId 는 숫자, stringId 를 string 이 type으로 추론된 것을 알 수 있다.
destructuring을 했을 때도 동일하고 당연히 string 과 number 를 비교할 때도 자료형이 다른 두 개를 비교할 수 없다고 오류가 나는걸 확인할 수 있다.
함수 타입 추론
const func1 = (a = 'a', b = 1) => {
return `${a} ${b};`;
};
func1(3, 6);
const v1: number = func1('a', 1);
func1 이라는 함수는 param a, b에 각각 default parameter를 사용했다.
func1 에 마우스를 올려보면 default parameter를 사용했기 때문에 값이 안들어올 경우도 대비해서 자동으로 a 를 string optional parameter, b 를 number optional parameter로 추론했다.
그리고 템플릿 리터럴을 반환하기 때문에 반환 값도 string으로 추론했다. 호출을 보면 a 는 string 이기 때문에 3이 들어갈수 없다고 알려주고 있다.
다음 줄 v1 를 보면 함수가 string을 반환하기 때문에 v1는 number가 될 수 없다고도 알려주고 있다.
만약 첫번째 param에 number를 넣고 싶다면 명시적으로 a: number | string 를 추가해서 number를 넣을 수 있다.
const func2 = (value: number) => {
if (value < 10) {
return value;
} else {
return `${value} is big`;
}
};
조건에 따라 number, string을 return 하고 있다. 이 경우에도 반환 값이 number 또는 string 인 것을 자동으로 잘 추론해주고 있다.