2022년 1월 19일 수요일

[번] JSON을 TypeScript 인터페이스로 Type-safely하게 Parse하기

원문 : https://dev.to/codeprototype/safely-parsing-json-to-a-typescript-interface-3lkj

 User라는 JSON 문자열을 반환하는 REST API를 이용한다고 가정해보자. JSON API는 문자열형의 firstNamelastName 그리고 number형인 accountBalance를 반환한다. User를 TypeScript를 사용해 아래와 같이 interface로 정의할 수 있다: 


interface User {

    firstName: string;

    lastName: string;

    accountBalance: number;

}


다음 코드는 JSON의 반환이 명시된 타입과 항상 일치한다고 하면 (happy path) 문제가 없다 :


const json = '{"firstName": "Kevin", "lastName": "Le", "accountBalance": 100}'

const user: User = JSON.parse(json)


하지만 API에서 에러가 발생할 수 있다. JSON 문자열이 다음과 같이 반환될 수 있다 :


const json  '{"firstName": "Kevin", "lastName": "Le", "accountBalance": "100"}'

const user: User = JSON.parse(json)

console.log(user)


accountBalance가 number가 아니라 any로 취급되나 에러가 발생하지 않는다. 그러나 이런 "너그러운" 조취는 다른 문제의 발생을 야기한다:


const balanceAfterInterest = user.accountBalance + user.accountBalance * 0.05

console.log(balanceAfterInterest) //1005


balanceAfterInterest 는 105가 되어야 한다. 더 나은 접근법은 이 문제를 catch하고 바로 적절하게 처리해주어야 한다.


https://app.quicktype.io에 접속하여 왼쪽 패널에 {"firstName": "Kevin", "lastName": "Le", "accountBalance": 100} 입력한다. 이름에 User를 입력하고, Source type으로 JSON 을 선택한다.

Alt Text

우측에서 LanguageTypeScript로 선택하고 Verify JSON.parse results at runtime이 설정돼있는지 확인한다. Quicktype은 생성된 코드와 사용방법을 중앙에 보여준다.

Alt Text

다음 코드는 안전하게 작동한다:
import { Convert, User } from "./user";

const json =
  '{"firstName": "Kevin", "lastName": "Le", "accountBalance": "100"}';

try {
  const user = Convert.toUser(json);
  console.log(user);
} catch (e) {
  console.log("Handle error", e);
}

예외가 발생하고 처리할 수 있다:



Quicktype이 생성한 코드는 다음과 같다 (user.js):
// To parse this data:

//

//   import { Convert, User } from "./file";

//

//   const user = Convert.toUser(json);

//

// These functions will throw an error if the JSON doesnt

// match the expected interface, even if the JSON is valid.



export interface User {

    firstName:      string;

    lastName:       string;

    accountBalance: number;

}



// Converts JSON strings to/from your types

// and asserts the results of JSON.parse at runtime

export class Convert {

    public static toUser(json: string): User {

        return cast(JSON.parse(json), r("User"));

    }



    public static userToJson(value: User): string {

        return JSON.stringify(uncast(value, r("User")), null, 2);

    }

}



function invalidValue(typ: any, val: any, key: any = ''): never {

    if (key) {

        throw Error(`Invalid value for key "${key}". Expected type ${JSON.stringify(typ)} but got ${JSON.stringify(val)}`);

    }

    throw Error(`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`, );

}



function jsonToJSProps(typ: any): any {

    if (typ.jsonToJS === undefined) {

        const map: any = {};

        typ.props.forEach((p: any) => map[p.json] = { key: p.js, typ: p.typ });

        typ.jsonToJS = map;

    }

    return typ.jsonToJS;

}



function jsToJSONProps(typ: any): any {

    if (typ.jsToJSON === undefined) {

        const map: any = {};

        typ.props.forEach((p: any) => map[p.js] = { key: p.json, typ: p.typ });

        typ.jsToJSON = map;

    }

    return typ.jsToJSON;

}



function transform(val: any, typ: any, getProps: any, key: any = ''): any {

    function transformPrimitive(typ: string, val: any): any {

        if (typeof typ === typeof val) return val;

        return invalidValue(typ, val, key);

    }



    function transformUnion(typs: any[], val: any): any {

        // val must validate against one typ in typs

        const l = typs.length;

        for (let i = 0; i < l; i++) {

            const typ = typs[i];

            try {

                return transform(val, typ, getProps);

            } catch (_) {}

        }

        return invalidValue(typs, val);

    }



    function transformEnum(cases: string[], val: any): any {

        if (cases.indexOf(val) !== -1) return val;

        return invalidValue(cases, val);

    }



    function transformArray(typ: any, val: any): any {

        // val must be an array with no invalid elements

        if (!Array.isArray(val)) return invalidValue("array", val);

        return val.map(el => transform(el, typ, getProps));

    }



    function transformDate(val: any): any {

        if (val === null) {

            return null;

        }

        const d = new Date(val);

        if (isNaN(d.valueOf())) {

            return invalidValue("Date", val);

        }

        return d;

    }



    function transformObject(props: { [k: string]: any }, additional: any, val: any): any {

        if (val === null || typeof val !== "object" || Array.isArray(val)) {

            return invalidValue("object", val);

        }

        const result: any = {};

        Object.getOwnPropertyNames(props).forEach(key => {

            const prop = props[key];

            const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined;

            result[prop.key] = transform(v, prop.typ, getProps, prop.key);

        });

        Object.getOwnPropertyNames(val).forEach(key => {

            if (!Object.prototype.hasOwnProperty.call(props, key)) {

                result[key] = transform(val[key], additional, getProps, key);

            }

        });

        return result;

    }



    if (typ === "any") return val;

    if (typ === null) {

        if (val === null) return val;

        return invalidValue(typ, val);

    }

    if (typ === false) return invalidValue(typ, val);

    while (typeof typ === "object" && typ.ref !== undefined) {

        typ = typeMap[typ.ref];

    }

    if (Array.isArray(typ)) return transformEnum(typ, val);

    if (typeof typ === "object") {

        return typ.hasOwnProperty("unionMembers") ? transformUnion(typ.unionMembers, val)

            : typ.hasOwnProperty("arrayItems")    ? transformArray(typ.arrayItems, val)

            : typ.hasOwnProperty("props")         ? transformObject(getProps(typ), typ.additional, val)

            : invalidValue(typ, val);

    }

    // Numbers can be parsed by Date but shouldnt be.

    if (typ === Date && typeof val !== "number") return transformDate(val);

    return transformPrimitive(typ, val);

}



function cast(val: any, typ: any): T {

    return transform(val, typ, jsonToJSProps);

}



function uncast(val: T, typ: any): any {

    return transform(val, typ, jsToJSONProps);

}



function a(typ: any) {

    return { arrayItems: typ };

}



function u(...typs: any[]) {

    return { unionMembers: typs };

}



function o(props: any[], additional: any) {

    return { props, additional };

}



function m(additional: any) {

    return { props: [], additional };

}



function r(name: string) {

    return { ref: name };

}



const typeMap: any = {

    "User": o([

        { json: "firstName", js: "firstName", typ: "" },

        { json: "lastName", js: "lastName", typ: "" },

        { json: "accountBalance", js: "accountBalance", typ: 0 },

    ], false),


};

댓글 없음:

댓글 쓰기

[번] Callback 지옥, Promises, 그리고 Async/Await

원문:  https://blog.avenuecode.com/callback-hell-promises-and-async/await  Callbacks와 promises, 그리고 async/await을 쓰는 비동기 자바스크립트는 반환하는데 시간이 소요되는...