동적인 데이터를 처리하는 다양한 방법.
동적인 데이터를 다뤄야 하는 상황
서버가 클라이언트로부터 데이터를 받는 지점이라 거나, npm jwt 라이브러리와 타입스크립트를 같이 사용하다 보면, 타입을 어떻게 해야 할지 고민이 생기는 곳이 있다.
// jwt는 내가 원하는 키:밸류 를 이용하여 서명을하고 검증을 할 수 있다.
token = jwt.sign({id: "1234"}, secret)
// 을 통해 얻은 token을 쿠키에 담아 클라이언트로 응답한다.
// 클라이언트는 서버에 요청할 때마다 쿠키를 같이 보내게 되고, 서버는 쿠키에서 토큰을 꺼내 검증한다.
result = jwt.verify(token, secret);
// 이 때, verify로 얻은 result에 id가 있는지 아닌지는 verfiy의 리턴 타입만으론 알 수 없다.
// verify의 리턴은 다음과 같다.
export interface JwtPayload {
[key: string]: any;
iss?: string | undefined;
sub?: string | undefined;
aud?: string | string[] | undefined;
exp?: number | undefined;
nbf?: number | undefined;
iat?: number | undefined;
jti?: string | undefined;
}
위 코드를 보면, JwtPayload 타입만으로는 jwt에 서명할 때 어떤 키를 사용했는지 알 수 없다. 익스프레스를 사용할 때, 리퀘스트의 바디에서 데이터를 뽑을 때도 동일하다. 외부(파일, 서버 등)로부터 입력을 받는 가장 경계선에선 항상 발생하는 상황이다. 그렇다면, 어떻게 타입을 명확하게 나타낼 수 있을까.
방법1 타입 단언(type assertion, casting)
1. 타입 단언(타입 캐스팅)
interface IPayload extends JwtPayload {
email: string;
}
result = jwt.verify(token, secret) as IPayload;
// 이후엔 result.email로 접근하여 사용할 수 있다.
// 하지만, 타입 캐스팅이기에 result에 정말 email 프로퍼티가 있는지는 모른다
// 다음 예제 코드를 보면, result.email이 없는게 확실하지만, 타입 단언은 이를 무시한다.
interface A {
id: string;
}
interface B extends A {
email: string;
}
function GetA(): A {
return {id: "MyId"};
}
const result = GetA() as B;
console.log(result.email); // undefined
방법2 in 연산자
in 연산자는 해당 key가 오브젝트에 존재하는 확인하는 연산자이다. in 연산자를 이용하면 type narrowing과 함께 실제로 객체 안에 원하는 프로퍼티가 있는지 확인할 수 있다.
interface A {
id: string;
}
interface B extends A {
email: string;
}
function GetA(): A {
return {id: "MyId"};
}
const result = GetA();
if('email' in result) {
console.log(result.email);
}
방법3 type predicate
방법2는 in 연산자를 활용하였다. 방법3 type predicate는 리턴 타입을 '파라미터 is 타입'으로 작성하는 방식이다.
interface IPayload extends Payload {
id: string;
}
function hasId(obj: any): obj is IPayload {
return (obj.id !== undefined);
}
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
if(hasId(decoded)){
// 타입이 좁혀져서 decoded.id 와같이 접근 가능하다
decoded.id = ...
}
방법4 assertion signature
리턴 타입을 이용하는 점에서 방법3과 비슷하다. 타입스크립트 3.7에 새로 소개된 기능으로, 다음과 같이 사용한다.
function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new AssertionError(msg);
}
}
function yell(str) {
assert(typeof str === "string");
// assert 이후로 str은 string으로 확실하게 좁혀진다
//아래 함수는 철자가 잘못되었으므로 타입스크립트 컴파일러가 에러를 잡아준다.
return str.toUppercase();
}
결론
위의 방법들 많고도 더 있어 보인다. 방법이 너무 많다. 이러한 방법을 모두 외우고 기억하는 것은 좋은 방향은 아닌 것 같다. 중요한 것은 타입을 한정 지을 수 있는 어떤 구문을 만나면(타입가드) 타입스크립트 컴파일러는 이를 분석하고 그다음 코드에서 타입을 좁힐 수 있다(타입내로잉)는 원리를 이해하고 자주 써보는 것으로 보인다.
타입 캐스팅이 가장 간단하지만, 객체에 내가 원하는 프로퍼티가 없을 수도 있으므로 확인하는 코드가 필요하다. 이러한 코드는 'key1' in obj, 'key2' in obj... 와 같은 코드 진행을 갖기에 번거로운 부분이 있다. class-transformer, class-validator 같은 라이브러리가 선호된 이유이기도 하다.