import type React from 'react';
import type {StyleXStyles} from '@stylexjs/stylex';

/**
 * Base props shared by all Astryx components.
 *
 * Omits props that are footguns, deprecated, or irrelevant to component APIs.
 * Components that genuinely need an omitted prop can declare it explicitly.
 */
export interface **BaseProps<T extends HTMLElement = HTMLElement>** extends Omit<
  **React.HTMLAttributes<T>,**
  | 'children'
  | 'title'
  | 'contentEditable'
  | 'dangerouslySetInnerHTML'
  | 'suppressContentEditableWarning'
  | 'suppressHydrationWarning'
  // Obscure
  | 'accessKey'
  | 'autoCapitalize'
  | 'autoFocus'
  | 'contextMenu'
  | 'enterKeyHint'
  | 'lang'
  | 'nonce'
  | 'slot'
  | 'spellCheck'
  | 'translate'
  | 'radioGroup'
  | 'inputMode'
  | 'is'
  // RDFa
  | 'about'
  | 'content'
  | 'datatype'
  | 'inlist'
  | 'prefix'
  | 'property'
  | 'rel'
  | 'resource'
  | 'rev'
  | 'typeof'
  | 'vocab'
  // Non-standard
  | 'autoCorrect'
  | 'autoSave'
  | 'color'
  | 'results'
  | 'security'
  | 'unselectable'
  // Microdata
  | 'itemProp'
  | 'itemScope'
  | 'itemType'
  | 'itemID'
  | 'itemRef'
  // Popover API (use Popover)
  | 'popover'
  | 'popoverTargetAction'
  | 'popoverTarget'
  // Shadow DOM
  | 'exportparts'
  // Form defaults
  | 'defaultChecked'
  | 'defaultValue'
> {
  /**
   * StyleX styles created via `stylex.create()`. Merged with the component's
   * base styles inside a single `stylex.props()` call for optimal deduplication.
   *
   * @example
   * ```
   * const overrides = stylex.create({ root: { marginBottom: 8 } });
   * <Component xstyle={overrides.root} />
   * ```
   */
  xstyle?: StyleXStyles;

  /** Allow data-* attributes for telemetry, testing, integration hooks, and
   * Astryx component prop/state reflection (for example data-variant/data-size). */
  [key: `data-${string}`]: string | undefined;
}

모든 컴포넌트의 prop은 항상 BaseProps를 상속한다.

CenterProps(어떠한 컴포넌트의 Prop)은 → BaseProps → div , span, h Element

CenterProps는 BaseProps<HTMLDivElement>를 상속하고, 그 BaseProps는 React의 HTMLAttributes에서 47개를 덜어낸 것입니다. extends HTMLAttributes는 "이 컴포넌트는 실제로 div이므로 div가 받는 것을 받는다"를 타입으로 선언한 것입니다. 이게 없으면 소비자가 습관처럼 div 의 onClick을 넘기는 순간 타입 에러가 나고, 컴포넌트마다 수백 개의 이벤트 핸들러와 aria 속성을 손으로 다시 적어야 합니다.

Omit이 실제로 막는 것?

사실 Omit이 막아주는 것은 런타임이나 실제 동작 과정이 아니긴 합니다. 단순히 코드를 작성하는 순간에 넣을 것과 아닌 것들을 알려주는 타입 유틸리티일 뿐입니다.

디자인 시스템을 통해서 막고자 하는 건 title이 물리적으로 DOM에 닿지 않는 상태가 아닙니다. 조금 TMI 지만, 그 중 하나인 title이 왜 차단 대상인지도 짚어둘 만합니다.

이는 당장은 중요하진 않지만, 접근성 측면에서 모바일에서는 안 뜨고, 스타일을 입힐 수 없고, 스크린리더마다 읽는 방식이 다릅니다. 이를 prop으로 열어두면 절반은 네이티브 툴팁으로 절반은 Tooltip 컴포넌트로 쓰게 되어 디자인 시스템이 갈라집니다. 이런 애매한 것들은 우선 공통적으로 차단하는 것이 곧 API 방향 지시인 셈입니다.

왜 HTMLElement 에서 빼는 방식인가

HTMLAttributes라는 친구가 가진 prop은 278개입니다. aria 속성 53개, 이벤트 핸들러를 포함한 DOM 속성 170개, 나머지 55개로 구성됩니다. BaseProps가 빼는 것은 47개이니 231개가 남습니다. 허용할 것만 적으려면 231줄을 써야 합니다. 타입까지 직접 선언한다면 React 타입 정의와 어긋날 위험도 떠안게 되고요. 차라리 짧은 쪽을 적는 게 자연스럽습니다.

막은 것들은 하나하나에 "왜 막았나" 이유가 전부 있고, PR에서 한 줄 추가되면 리뷰어가 그 이유를 물을 수 있습니다. 그러나 아이러니하게, 반대로 231개짜리 허용 목록은 그냥 명단입니다. onPointerEnter가 왜 거기 있냐고 물으면 "HTML 표준이라서"라는 답을 할 수밖에 없습니다.

children → 옵셔널? → 그 하위에 항상 뭐가 들어갈 수 있다. → <Spinner/>

children을 빼는 이유

Omit 목록에 children이 들어 있는 것도 처음엔 의아했습니다. HTMLAttributes의 children은 옵셔널인데, CenterProps는 이걸 필수로 다시 선언합니다. children 없는 Center는 의미가 없으니까요. 상속받은 옵셔널을 그대로 두면 재선언이 충돌하거나 약해집니다. 뜯어내야 각 컴포넌트가 자유롭게 정할 수 있습니다. 필수로 만들거나, 아예 선언하지 않아 금지하거나, 특정 자식 타입만 받도록 좁히거나.

제네릭에 대한 오해

BaseProps<T extends HTMLElement>의 제네릭이 태그별로 prop을 걸러주는 장치라고 생각했는데, 확인해보니 아니었습니다. keyof HTMLAttributes<HTMLDivElement>와 keyof HTMLAttributes<HTMLInputElement>는 완전히 같은 집합입니다. href나 value 같은 요소 고유 속성은 애초에 HTMLAttributes에 들어 있지 않아서 걸러낼 것도 없습니다.

그런 속성들은 AnchorHTMLAttributes나 InputHTMLAttributes가 HTMLAttributes를 상속하면서 따로 얹는 구조입니다.

그러면 제네릭은 무엇을 하냐면, 이벤트 객체의 currentTarget 타입만 좁힙니다. React 타입 정의에서 SyntheticEvent<T>의 currentTarget이 EventTarget & T로 되어 있어서, T가 HTMLInputElement여야 핸들러 안에서 e.currentTarget.value를 캐스팅 없이 쓸 수 있습니다. 참고로 target은 자식 요소일 수 있어 의도적으로 좁히지 않습니다. prop 목록과 제네릭은 아예 다른 축이었습니다.

두 결정의 공통점

정리하고 보니 스타일 쪽과 prop 쪽이 같은 성격의 판단이었습니다. 둘 다 충돌이나 권한 문제를 가능한 한 이른 단계에서, 사람이 조심하는 대신 도구가 검사하는 자리로 옮겨놓습니다. StyleX는 CSS 캐스케이드에서 벌어질 싸움을 JS 객체 스프레드로 끌어내렸고, BaseProps는 런타임에 무엇이 DOM에 닿는지를 걱정하는 대신 호출부에서 타입으로 끊었습니다.

대가도 각각 있습니다. StyleX는 스타일이 정적으로 분석 가능해야 한다는 제약을 겁니다. stylex.create는 모듈 최상위에서만 호출할 수 있고 값도 리터럴이어야 합니다. BaseProps는 빼는 방식이라 React 타입이 업데이트되며 새 HTML 속성이 생기면 자동으로 문제가 생길 수 있습니다. 조용히 업데이트 되더라도 컴파일은 통과합니다. 파일 헤더에 무엇을 유지하고 무엇을 뺐는지 주석으로 적어둔 것이 그 빈틈을 사람 리뷰로 메우려는 흔적으로 보입니다.