본문으로 건너뛰기
jsonbeautifiers
한국어

JSON을 TypeScript로

배열의 모든 요소를 합쳐 인터페이스를 만들기 때문에 선택 필드가 진짜 선택 필드가 됩니다.

JSON
TypeScript

붙여 넣은 것은 여러분의 브라우저를 떠나지 않습니다. connect-src 허용 목록 덕분에 이는 약속이 아니라 브라우저가 강제하는 보장입니다. 직접 확인하기

JSON 페이로드를 TypeScript 인터페이스로 바꿉니다. 중첩된 객체는 각자 이름 있는 타입이 되고, 배열은 모든 요소를 합쳐서 보고, 구조가 똑같은 형태는 한 번만 만들어 재사용합니다.

샘플에서 추론하는 건 어차피 추측입니다. 이 페이지의 요점은 그 추측이 눈에 보인다는 것입니다.

선택 필드, 대부분의 생성기가 무너지는 지점

배열의 0번 요소만 읽고 멈추는 생성기는 모든 필드가 필수인 인터페이스를 만들어 냅니다. 그러면 선택 필드를 빠뜨린 첫 레코드에서 타입 검사가 실패하고, 결국 생성된 타입을 손으로 고치게 되어 애초의 의미가 사라집니다.

이 도구는 모든 요소를 합칩니다. 전부에 있는 키는 필수, 일부에만 있는 키는 물음표를 붙여 선택으로 표시합니다. 그건 여러분의 샘플에서 뽑아낸 진짜 정보이고, 요소 하나만 보는 생성기는 그걸 버립니다.

나머지 결정들

null은 유니온이 됩니다
문자열로도 null로도 관측된 필드는 string | null입니다. any도 string도 아닙니다.
빈 배열은 unknown[]입니다
any[]가 아닙니다. 빈 배열은 타입 정보를 담고 있지 않고, any[]로 두면 그것에 닿는 모든 것의 검사를 조용히 꺼 버립니다.
반복되는 형태는 중복 제거합니다
레코드 500개짜리 목록에서 인터페이스는 500개가 아니라 하나가 나옵니다.
배열 요소 이름은 단수로 만듭니다
categories 배열에서는 Category 인터페이스가 나옵니다.
유효하지 않은 식별자는 따옴표로 감쌉니다
has-dash, 2fast, class 같은 키는 따옴표가 붙은 속성 이름이 됩니다.
안전하지 않은 정수는 표시합니다
샘플에 2^53-1을 넘는 정수가 있었는데 필드를 number로 잡는 건 거짓말입니다. JavaScript가 그 값을 표현하지 못하기 때문입니다. 노트가 그렇게 알려 줍니다. 올바른 해결은 위쪽에서 문자열로 보내는 것입니다.

생성된 타입이 주지 못하는 것

TypeScript 인터페이스는 런타임에 지워집니다. 컴파일러에게 무엇을 기대하는지 알려 줄 뿐, API가 다른 걸 보내 왔을 때 아무 일도 하지 않습니다. 그건 런타임 검증기의 몫이고, 외부 API를 다루는 정직한 방식은 같은 정의에서 검증과 타입 추론을 함께 얻는 스키마를 쓰는 것입니다.

Zod, Valibot, ArkType 모두 그렇게 합니다. 페이로드를 이해하려면 여기서 인터페이스를 생성하고, 통제할 수 없는 경계에는 런타임 스키마를 쓰세요.

How to do this in code

같은 발상을 코드로, 그리고 믿을 수 없는 경계에서 무엇을 쓸지.

sh quicktype

quicktype은 여러 대상 언어를 지원합니다. 정말 중요한 건 샘플 여러 개를 넘기는 옵션입니다.

npx quicktype --lang ts --just-types --src-lang json payload.json

# Several samples, which is what makes optionality accurate
npx quicktype --lang ts --just-types samples/*.json
ts Zod

API 경계에서는 이 패턴을 쓰세요. 인터페이스는 컴파일러에게 말하고, 스키마는 사실을 말합니다.

import { z } from 'zod';

const User = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  verifiedAt: z.string().nullable(),
  roles: z.array(z.string()),
});

// One definition, both a runtime check and a static type
type User = z.infer<typeof User>;

const result = User.safeParse(await res.json());
if (!result.success) console.error(result.error.issues);
ts 리터럴에서 타입만
// If the data is a constant you control, TypeScript can infer
// the type without a generator at all.
const config = {
  retries: 3,
  endpoints: ['a', 'b'],
} as const;

type Config = typeof config;

자주 묻는 질문

제 API는 항상 보내는데 왜 그 필드가 선택으로 나오나요?
샘플의 레코드 중 적어도 하나에 그 필드가 없었기 때문입니다. 진짜 선택 필드이거나, 샘플이 불완전한 것입니다. 레코드를 더 담은 배열을 붙여 넣으면 답이 좋아집니다.
interface와 type 중 무엇을 써야 하나요?
객체 형태를 적는 데는 거의 차이가 없습니다. interface는 선언 병합이 되고 오류 메시지가 조금 더 낫습니다. type 별칭은 유니온과 매핑된 타입을 표현할 수 있습니다. 위에서 둘 다 고를 수 있습니다.
이 타입들이 런타임에 뭔가를 검증하나요?
아닙니다. TypeScript 타입은 컴파일할 때 지워집니다. 여러분이 믿는 바를 기술할 뿐, 확인하지는 않습니다. 네트워크 경계를 넘는 데이터에는 런타임 검증기를 쓰세요.