제네릭은 어떠한 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 프로그래밍 기법을 말한다.
제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다. 한번의 선언으로 다양한 타입에 재사용이 가능하다는 장점이 있다.
다양한 타입을 가지는 경우 아래와 같이 나열하여 사용할 수도 있다.
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);
아래의 예시에서 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 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',
};
아래의 코드에서 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);
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이 없기 때문)
// 문자열이든 배열이든 상관없이,
// 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
아래와 같이 입력하면 입력한 객체가 무엇이든 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 타입이 유효하지 않게 된다.
파셜 타입은 특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있다.
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;
}
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'
};
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
}
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,
},
};
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>'.
};
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">'.
};
유니언 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;
유니언 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'.
유니언 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
함수 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'.