0. Table Of Contents
1. 서론
class 정의시, constructor를 정의하곤 했다. 일반적인 constructor를 생성시, 아래와 같은 형태를 띈다
export class ModbusConnection {
constructor(modbusClient: ModbusRTU, deviceId: any, ipAddress: string, modelId: any, nestedDeviceIdList: Device[]) {
this.modbusClient = modbusClient;
this.deviceId = deviceId;
this.ipAddress = ipAddress;
this.modelId = modelId;
this.nestedDeviceIdList = nestedDeviceIdList;
}
modbusClient: ModbusRTU;
deviceId: any;
ipAddress: string;
modelId: any;
nestedDeviceIdList: Device[];
}
생성자로 인자를 받아 그대로 객체를 생성하는 것임에도 불구하고 불필요하게 코드가 자꾸 사용되는것 같다는 생각이 들었다. 이를 어떻게 하면 코드를 좀 더 간결하게 줄일 수 있을 까 생각하게 되었으며 이를 해결하기 위해 typescript의 generic을 사용하여 해보기로 하였다.
2. 이번 문제의 key는 왜 Generic인가?
한가지 타입이 아닌 여러가지 타입을 기반으로 동작하는 function을 설계할 때 주로 사용한다. 이와 비슷한 역할을 하는 any 타입으로 대체할 수 있었지만, 나는 아래와 같은 이유로 generic을 사용하게 되었다. generic 뜻을 찾아보니 '데이터 타입을 일반화 시키는 것'이라고 한다. generic을 이용하면 아래와 같이 개선할 수 있다고 생각이 들었다.
- any를 사용하면 어떠한 타입이라도 모두 받을 수 있지만, 어떤 타입이 들어갔는지에 대해 알 수 없다.
- any는 타입 체크를 하지 않기 때문에 typescript의 최고 장점인 컴파일에서 오류를 잡을 수 없다는 단점이 존재한다.
- 그러나 generic은 개발자로부터 이러한 타입을 쓸 것이다 라는 것을 받기 때문에 컴파일 단계에서 오류도 잡을 수 있고 유동적으로 타입을 변경할 수 있다고 생각했다.
3. Generic
위에서 언급했지만, 한 번 더 정리하면 ‘데이터 타입을 일반화 시키는 것’을 의미한다. 기본적인 Generic을 Typescript화 시키면, 아래와 같이 사용할 수 있다.
function identity<T>(argument: T): T {
return arg;
}
T는 Type을 지칭하는 약어로, 통상적인 약속으로 T라고 명명한다.
generic을 이용할 시, extends를 이용하여 특정 object가 가진 property들에 한정시킬 수도 있다.
interface Item {
name: string;
price: number;
stock: number;
}
function identity<T extends keyof Item>(argument: T): T {
return argument;
}
4. Generic Type (Utility Type)
https://www.typescriptlang.org/docs/handbook/utility-types.html페이지에 많은 유틸리티 타입이 존재하지만, 이 중 내가 이번에 사용한 type만 서술해본다.
4.1. Partial<T>
입력되는 특정 type의 부분집합을 type으로 구성 할 수 있다.
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "organize desk",
description: "clear clutter",
};
const todo2 = updateTodo(todo1, {
description: "throw out trash",
});
4.2. Pick<T, Keys>
특정 Type의 특정 Property를 이용하여 새로운 타입으로 정의할 수 있다.
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
5. 최종 적용 코드
Class를 선언하게 되면 this를 사용할 수 있기 때문에, Object.assign을 이용하여 한번에 입력 할 수 있을 것이라고 생각했으며, 간혹 어쩔 수 없이 any를 사용하는 경우, function이 들어가는 case를 막기 위해 아래와 같은 코드를 생각하게 되었다.
type ClassProperties<T> = Pick<T, { [P in keyof T]: T[P] extends Function ? never : P }[keyof T]>;
위 코드를 설명하자면 아래왜 같이 설명 할 수 있다.
[입력된 type의 Property] : Type의 Property가 Function type을 상속 받으면 ? 내보내지 않는다 : type
즉, 입력된 type의 property 중 function type을 제외한 모든 property가 pick되는 구조이다. 이를 이용하여 위 코드를 아래와 같이 적
용 할 수 있었다.
export class ModbusConnection {
constructor(data: ClassProperties<ModbusConnection>) {
Object.assign(this, data);
}
modbusClient: ModbusRTU;
deviceId: any;
ipAddress: string;
modelId: any;
nestedDeviceIdList: Device[];
}
constructor에서 약간의 연산이 들어가면 위 코드 중 constructor 쪽이 조금 더 길어지겠지만, constructor 인자로 받는 부분과 this.{property}=arg가 없어지니 코드가 간결해졌으며 보기 좋아진거같다.