- Type & Generic
TS 코드를 작성함에 있어 타입을 명확히 하지 않고 any 를 통해 느슨한 타입을 명시하는 코드는 생각보다 많습니다.
const add = (a: any, b: any) => a + b;
console.info(add(3, 4)); // 7
console.info(add('3', 4)); // '34'
console.info(add(3, '4')); // '34'
console.info(add('3', '4')); // '34'
좀 억지스러운 예제일 수 있지만, 이런 단순한 함수를 작성할 때도 a, b 에 any 타입을 넣는 개발자는 꽤 많습니다.
실제로 이 코드는 a, b 에 any 타입을 받을 수 있으니 string 이나 number 가 들어와도 상관이 없으며 별다른 에러 없이 실행도 잘 됩니다.
자바스크립트에서 + 연산 시 일어나는 자동 타입 캐스팅에 의해서 말이죠.
const add = (a: any, b: any) => {
return Number(a.toFixed(2)) + Number(b.toFixed(2));
};
console.info(add('3', '4')); // TypeError: a.toFxied is not a function
하지만 만약 이런 코드가 들어간다면 해당 함수에 '3', '4' 와 같은 string 이 인자로 들어왔을 때 에러가 난다는 걸 런타임때나 알게 될 것입니다. 물론 toFixed 메서드가 Number 객체에만 있다는 걸 알면 애초에 이런 실수가 발생하지 않겠지만 이는 단순 예시일뿐, 실제 복잡한 코드에선 얼마든지 일어날 수 있는 문제입니다. 그래서 개발자들은 정확히 타입을 명시해 컴파일 단계에서 이러한 문제를 잡고자 합니다.
const add = (a: number, b: number) => {
return Number(a.toFixed(2)) + Number(b.toFixed(2));
};
console.info(add('3', '4')); // error TS2345: Argument of type 'string' is not assignable to parameter of type 'number
any 타입으로 되어있던 a, b 두 파라미터를 number 타입으로 명시해줬습니다. 런타임 시 겪게 될 문제를 타입을 명시함으로써 컴파일 단계로 당겨오게 되었습니다.
하지만 이런 생각을 해볼 수 있습니다. add 함수의 두 파라미터인 a, b 가 다양한 타입을 받게 하고 싶어서 any 타입을 사용했지만 이럴 경우 타입 안정성이 떨어지다보니 결국 하나의 타입만 명시할 수 밖에 없는 모순이 생기는 것입니다.
우리는 이러한 문제를 Generic 을 사용해 해결할 수 있습니다. 뿐만 아니라 컴파일 시 타입 안정성을 가져가면서도 타입 캐스팅과 같은 코드를 제거할 수 있게 됩니다.
function add<T>(a: T, b: T) {
return Number(a) + Number(b);
}
console.info(add<number>(3, 4)); // 7
console.info(add<string>('3', '4')); // 7
먼저 TS 에서 Generic 은 위와 같이 작성합니다. T 는 타입 매개변수 (혹은 제네릭 타입 변수) 라고 불리며 주로 T 로 표기하지만 다른 문자를 사용해도 상관없습니다. 작성한 타입 매개변수를 기준으로 컴파일 시 타입 바인딩을 통해 a, b 의 타입이 정해집니다.
따라서 add<number> 에는 number 타입의 변수만 넣을 수 있고, add<string> 에는 string 타입의 변수만 넣을 수 있습니다.
add<string>(3, 4) 와 같은 코드를 작성하면 컴파일 에러가 나는 것을 볼 수 있습니다.
하지만 여기서 드는 의문은 코드에 존재하는 타입 캐스팅에 대한 부분입니다.
위 코드에선 Number(a) 처럼 타입 캐스팅 된 부분을 제거하면 컴파일 에러가 발생합니다. T 타입 매개변수에 + 연산자를 적용할 수 없기 때문이죠. 이 부분은 타입 상속을 통해 해결할 수 있습니다.
function add<T extends number>(a: T, b: T) {
return a + b;
}
console.info(add<number>(3, 4)); // 7
타입 매개변수 T 가 number 타입을 상속하게 함으로써 Number 타입 캐스팅 코드를 제거할 수 있게 되었습니다.
이렇게 선언하면 T 의 타입을 제한하는 의미를 갖기도 합니다.
- Generic 활용(1)
Generic 을 통해 컴파일 시 타입 안정성을 가져가는 부분은 당연하며 제가 실제 코드에서 적용하고 있는 좀 더 실무적인 예시를 들어볼까 합니다.
서버 프로그램 작성 시 DB 를 사용하지 않는 코드는 거의 없는데, 이런 경우 보통 ORM 을 사용하지 않고 일반적인 mysql 모듈을 사용하는 서버라면 raw query 를 통해 얻은 결과값을 사용할 때 타입의 도움을 받기가 어렵습니다. 이런 경우 Generic 을 활용하곤 합니다.
class DBClient {
...
query<ResultType>(queryStr: string, option: any[]): Promise<ResultType> {
...
}
...
}
const getIds = async () => {
const client = new DBClient(...);
const query = `
SELECT id
FROM ...
`;
const rows = await client.query<Array<{ id: string }>>(query, []);
return rows.map(row => {
const { id } = row;
return id;
};
};
...
코드의 많은 부분이 생략되어있지만 DB 에 쿼리해 결과값을 얻어오는 메서드인 query 에 ResultType 타입 매개변수를 정의해 메서드를 호출하는 부분에서 쿼리 결과값에 대한 타입을 정의해 활용할 수 있도록 Generic 을 사용한 예시입니다.
쿼리에 대한 결과 타입을 Array<{ id: string }> 로 명시했기에, 이후 rows.map 부분에서도 row 에서 id 타입 추론의 도움을 받을 수 있습니다. 물론 쿼리를 잘못 작성했다면 getIds 함수가 반환하는 배열엔 undefined 만 들어있을 수도 있고, 저런 타입 매개변수가 없더라도 쿼리만 잘 작성했다면 컴파일 & 런타임 시 아무런 문제가 없을 수 있습니다.
하지만 위와 같이 Generic 을 활용함으로써 타입 안정성에 대한 부분과 더불어 IDE 내에서 생산성 향상도 분명 체감될 것 입니다.
- Generic 활용(2)
이번 예시에선 이전 포스팅에서 작성한 aws-redshift 모듈 타입 정의 부분을 좀 더 확장해보도록 하겠습니다.
declare module 'aws-redshift' {
class Redshift {
...
query(queryString: string, option?: any): Promise<any>;
...
}
export = Redshift;
}
aws-redshift 에 대한 타입을 정의할 때 실제 query 메서드가 받는 인자와 반환 타입을 고려해 query 메서드에 대한 타입을 정의했습니다.
좀 더 확장하려는 부분은, aws-redshift 모듈이 제공하는 Redshift 객체의 query 메서드는 실제로 option 값에 따라 반환하는 타입이 달라집니다. option 중 raw 값을 true 로 보내면 쿼리 조건에 부합하는 rows 만 반환하고, true 가 아닐 경우엔 rows 를 포함해 command, rowCount 등 PGResultType 이 반환됩니다. 이 부분을 Generic 을 활용해 정의해보려는 것 입니다.
type QueryOption = {
raw: boolean;
};
type PGResultType = {
command: string;
rowCount: number | null;
oid: any;
rows: any;
fields: Array<any>;
};
declare module 'aws-redshift' {
class Redshift {
...
query<T extends undefined | QueryOption>(queryString: string, option?: T): T['raw'] extends true ? Promise<any> : Promise<PGResultType>;
...
}
export = Redshift;
}
여기서도 Generic 의 타입 상속을 활용했습니다. query 메서드 호출 시 두번째 인자로 넘어올 option 값은 optional 이므로 타입 매개변수 T 는 undefined 혹은 { raw: boolean } 타입을 상속하도록 합니다. 그리고 반환 타입 정의시 T 의 raw 값이 true 를 상속하고 있다면 Promise<any>, 그 외의 경우엔 Promise<PGResultType> 을 반환하도록 작성한 것이죠.
실제로 문제 없는 코드이며 타입 상속에도 삼항 연산자를 활용할 수 있는 점을 알 수 있는 예시입니다.
실제 예시를 보면 알 수 있듯이 option 에 넘기는 { raw: true } 값에 따라 결과값에 대한 타입 추론이 any 로 되었습니다.
raw 값이 true 를 상속할 경우의 반환 타입을 Promise<any> 로 정의했기 때문입니다.
여기선 query 메서드 호출 시 option 을 넘기지 않았고, 그에 따라 결과 타입이 PGResultType 으로 추론되어 해당 타입에 정의되어 있던 command, fields, oid 등의 property 에 접근 가능하다는 IDE 의 도움을 받은 예시입니다.
이런식으로 TS 에서 Generic 을 활용해 좀 더 다채로운 코드를 작성할 수 있습니다.
'Backend > Typescript' 카테고리의 다른 글
Typescript 외부 모듈 타입 선언 (0) | 2021.12.11 |
---|