본문으로 건너뛰기

JSX_TypeScript 노트 17

무료2019-04-27#TypeScript#tsx#TypeScript JSX#tsx props type#TypeScript React#tsx 类型检查

TS 도 JSX 에 대해 완비된 타입 서포트를 제공합니다

一.기본 용법

TypeScript 도 JSX 를 서포트하며, Babel 과 마찬가지로 JSX 를 JavaScript 로 컴파일할 수 있을 뿐만 아니라, 타입 체크도 제공합니다

TypeScript 로 JSX 를 쓰려면 2 스텝만 필요합니다:

  • 소스 파일에 .tsx 확장자 사용

  • --jsx 옵션을 유효화

게다가, TypeScript 는 3 종류의 JSX 처리 모드를 제공하며, 각각 다른 코드 생성 룰에 대응합니다:

ModeInputOutputOutput File Extension
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js

즉:

  • preserve: .jsx 파일을 생성하지만, JSX 구문을 변환하지 않고 유지하며, 후속 빌드環節 (Babel 등) 에 처리를 위임합니다

  • react: .js 파일을 생성하며, JSX 구문을 React.createElement 로 변환합니다

  • react-native: .js 파일을 생성하지만, JSX 구문을 변환하지 않고 유지합니다

이들 모드는 --jsx 옵션으로 지정하며, 디폴트는 "preserve" 로, 코드 생성에만 영향주고, 타입 체크에는 영향주지 않습니다 (예를 들어 --jsx "preserve" 는 변환하지 않는 것을 요구하지만, 그래도 JSX 에 대해 타입 체크를 수행합니다)

구체적 사용에서는, JSX 구문은 완전히 일치하며, 유일하게 주의가 필요한 것은 타입 어설션입니다

타입 어설션

JSX 에서는 as type 만 사용할 수 있습니다 (산괄호 구문이 JSX 구문과 충돌하기 때문)

let someValue: any = "this is a string";
// <type>
let strLength: number = (<string>someValue).length;

.tsx 파일에서는 에러가 발생합니다:

JSX element 'string' has no corresponding closing tag.

'</' expected.

구문의 충돌로 인해, <string>someValue 중의 타입 어설션 부분 (<string>) 이 JSX 요소로서 취급됩니다. 따라서 .tsx 에서는 as type 형식의 타입 어설션만 사용할 수 있습니다:

// as type
let strLength: number = (someValue as string).length;

P.S. TypeScript 타입 어설션의 더 많은 정보는, [三。타입 어설션](/articles/기본 타입-typescript 노트 2/#articleHeader4) 참조

二.요소 타입

JSX 식 <expr /> 에 대해, expr 는 환경 중의 내재 요소 (intrinsic element, 즉 내장 컴포넌트, 예를 들어 DOM 환경 중의 div 또는 span) 또는 값 기반 요소 (value-based element), 즉 커스텀 컴포넌트 중 하나입니다. 2 개의 요소의 차이는:

  • 생성되는 타겟 코드가 다름

    React 에서는, 내재 요소는 문자열을 생성합니다 (예를 들어 React.createElement("div")), 커스텀 컴포넌트는 생성하지 않습니다 (예를 들어 React.createElement(MyComponent))

  • 요소 속성 (즉 Props) 타입의 검색 방법이 다름

    내재 요소의 속성은 알려져 있지만, 커스텀 컴포넌트는 자신의 속성 세트를 지정하고 싶을 수 있습니다

형식상으로는, 커스텀 컴포넌트는 반드시 대문자로 시작해야 합니다, 이것으로 2 개의 JSX 요소를 구분합니다

P.S. 실제로는, 내재 요소/값 기반 요소와 내장 컴포넌트/커스텀 컴포넌트는 같은 것을 지칭하며, TypeScript 컴파일러에게, 내장 컴포넌트의 타입은 알려져 있어, 이를 내재 요소라고 부르고, 커스텀 컴포넌트의 타입은 컴포넌트 선언 (값) 과 관련되어, 이를 값 기반 요소라고 부릅니다

내재 요소

내재 요소의 타입은 JSX.IntrinsicElements 인터페이스에서 검색합니다. 이 인터페이스를 선언하지 않은 경우, 모든 내재 요소는 타입 체크되지 않습니다. 선언한 경우, JSX.IntrinsicElements 상에서 대응하는 속성을 검색하여, 타입 체크의 근거로 합니다:

declare namespace JSX {
  interface IntrinsicElements {
    foo: any
  }
}

// 올바른
<foo />;
// 에러 Property 'bar' does not exist on type 'JSX.IntrinsicElements'.
<bar />;

물론, 인덱스 시그니처 와 조합하여, 미지의 내장 컴포넌트의 사용을 허가할 수도 있습니다:

declare namespace JSX {
  interface IntrinsicElements {
    foo: any;
    [elemName: string]: any;
  }
}

// 올바른
<bar />;

이점은, 장래 새로운 내장 컴포넌트의 서포트를 확장한 후, 바로 타입 선언을 수정할 필요가 없는 것으로, 대가는 화이트리스트의 엄격한 검증을 잃는 것입니다

값 기반 요소

값 기반 요소는 직접 스코프 내로부터 대응하는 식별자를 찾습니다. 예를 들어:

import MyComponent from "./myComponent";

// 올바른
<MyComponent />
// 에러 Cannot find name 'SomeOtherComponent'.
<SomeOtherComponent />

값 기반 요소는 2 종류 있습니다:

  • 스테이트리스 함수형 컴포넌트 (Stateless Functional Component, 소위 SFC)

  • 클래스 컴포넌트 (Class Component)

이들 2 つ는 JSX 식의 형식상으로는 구분할 수 없으므로, 먼저 SFC 로서 함수 오버로드에 따라 해석을 시도하고, 해석에 실패한 경우에 클래스 컴포넌트로서 처리하며, 더욱 실패하면 에러가 됩니다

스테이트리스 함수형 컴포넌트

형식상으로는 일반적인 함수로, 첫 번째 파라미터는 props 오브젝트이며, 반환 타입은 JSX.Element(또는 그 [서브타입](/articles/深入 타입 시스템-typescript 노트 8/#articleHeader4)) 일 필요가 있습니다. 예를 들어:

function Welcome(props: { name: string }) {
  return <h1>Hello, {props.name}</h1>;
}

동様に, [함수 오버로드](/articles/함수-typescript 노트 5/#articleHeader9) 도 적용됩니다:

function Welcome(props: { content: JSX.Element[] | JSX.Element });
function Welcome(props: { name: string });
function Welcome(props: any) {
  <h1>Hello, {props.name}</h1>;
}

<div>
  <Welcome name="Lily" />
  <Welcome content={<span>Hello</span>} />
</div>

P.S. JSX.Element 타입 선언은 [@types/react](https://www.npmjs.com/package/ @types/react) 에서 왔습니다

클래스 컴포넌트

클래스 컴포넌트는 React.Component 를 상속하며, JavaScript 판과 거의 차이가 없습니다:

class WelcomeClass extends React.Component {
  render() {
    return <h1>Hello, there.</h1>;
  }
}

<WelcomeClass />

Class 의 [이중 타입 의미](/articles/클래스-typescript 노트 4/#articleHeader8) 와 유사하게, JSX 식 <Expr /> 에 대해, 클래스 컴포넌트의 타입은 2 부분으로 나뉩니다:

  • 요소 클래스 타입 (element class type): Expr 의 타입, 즉 typeof WelcomeClass

  • 요소 인스턴스 타입 (element instance type): Expr 클래스 인스턴스의 타입, 즉 { render: () => JSX.Element }

예를 들어:

// 요소 클래스 타입
let elementClassType: typeof WelcomeClass;
new elementClassType();
// 요소 인스턴스 타입
let elementInstanceType: WelcomeClass;
elementInstanceType.render();

요소 인스턴스 타입은 JSX.ElementClass 의 서브타입일 필요가 있으며, 디폴트의 JSX.ElementClass 타입은 {} 로, React 에서는 render 메서드를 가질 필요가 있습니다:

namespace JSX {
  interface ElementClass extends React.Component<any> {
    render(): React.ReactNode;
  }
}

(DefinitelyTyped/types/react/index.d.ts 에서 발췌)

否则 에러가 됩니다:

class NotAValidComponent {}
function NotAValidFactoryFunction() {
  return {};
}

<div>
  {/* 에러 JSX element type 'NotAValidComponent' is not a constructor function for JSX elements. */}
  <NotAValidComponent />
  {/* 에러 JSX element type '{}' is not a constructor function for JSX elements. */}
  <NotAValidFactoryFunction />
</div>

三.속성 타입

속성 체크는 먼저 요소 속성 타입 (element attributes type) 을 확정할 필요가 있으며, 내재 요소와 값 기반 요소는 속성 타입에 조금 차이가 있습니다:

  • 내재 요소의 속성 타입: JSX.IntrinsicElements 상 대응하는 속성의 타입

  • 값 기반 요소 속성 타입: 요소 인스턴스 타입 상 특정 속성 타입 상 대응하는 속성의 타입, 이 특정 속성은 JSX.ElementAttributesProperty 로 지정

P.S. JSX.ElementAttributesProperty 를 선언하지 않은 경우, 컴포넌트 클래스 생성자 또는 SFC 첫 번째 파라미터의 타입을 취득합니다

구체적으로는, 내재 요소 속성은 ahref 를 예로:

namespace JSX {
  interface IntrinsicElements {
    // 각 내재 요소及其び그 속성 타입을 선언
    a: {
      download?: any;
      href?: string;
      hrefLang?: string;
      media?: string;
      rel?: string;
      target?: string;
      type?: string;
      referrerPolicy?: string;
    }
  }
}

// 요소 속성 타입은 { href?: string }
<a href="">링크</a>

값 기반 요소 속성은 예를 들어:

namespace JSX {
  // 특정 속성명을 props 로 지정
  interface ElementAttributesProperty { props: {}; }
}

class MyComponent extends React.Component {
  // 속성 타입을 선언
  props: {
    foo?: string;
  }
}
// 요소 속성 타입은 { foo?: string }
<MyComponent foo="bar" />

옵션 속성, 전개 연산자 등도 동일하게 적용되며, 예를 들어:

class MyComponent extends React.Component {
  // 속성 타입을 선언
  props: {
    requiredProp: string;
    optionalProp?: string;
  }
}

const props = { optionalProp: 'optional' };
// 올바른
<MyComponent { ...props } requiredProp="required" />

P.S. 게다가, JSX 프레임워크는 JSX.IntrinsicAttributes 를 통해 프레임워크에 필요한 추가 속성을 지정할 수 있습니다. 예를 들어 React 내의 key. 상세는 Attribute type checking 참조

P.S. 特殊的에는, 속성 체크는 속성명이 합법한 JavaScript 식별자의 속성에 대해서만 수행되며, data-* 등은 체크되지 않습니다

자식 컴포넌트 타입 체크

자식 컴포넌트의 타입은 요소 속성 타입 상의 children 속성에서 오며, ElementAttributesPropertyprops 를 지정하는 것과 마찬가지로, 여기서는 JSX.ElementChildrenAttribute 를 사용하여 children 을 지정합니다:

namespace JSX {
  // 특정 속성명을 children 으로 지정
  interface ElementChildrenAttribute { children: {}; }
}

const Wrapper = (props) => (
  <div>
    {props.children}
  </div>
);
<Wrapper>
  <div>Hello World</div>
  {"This is just a JS expression..." + 1000}
</Wrapper>

children 에 타입을 지정하는 방법은 일반적인 속성과 유사합니다:

interface PropsType {
  children: JSX.Element
  name: string
}
class Component extends React.Component<PropsType, {}> {
  render() {
    return (
      <h2>
        {this.props.children}
      </h2>
    )
  }
}

<Component name="hello">
  <h1>Hello World</h1>
</Component>

자식 컴포넌트 타입이 일치하지 않으면 에러가 됩니다:

// 에러 Type '{ children: Element[]; name: string; }' is not assignable to type 'Readonly<PropsType>'.
<Component name="hello">
  <h1>Hello World</h1>
  <h1>Hi</h1>
</Component>

四.결과 타입

디폴트에서는, JSX 식의 결과 타입은 any 입니다:

// a 의 타입은 any
let a = <a href="" />;
a = {};

JSX.Element 를 통해 지정할 수 있습니다. 예를 들어 React 에서는:

let a = <a href="" />;
// 에러 Type '{}' is missing the following properties from type 'Element': type, props, key.
a = {};

대응하는 타입 선언은 유사합니다:

namespace JSX {
  interface Element<T, P> {
    type: T;
    props: P;
    key: string | number | null;
  }
}

P.S. React 내의 구체적인 JSX 요소 타입 선언은 DefinitelyTyped/types/react/index.d.ts 참조

五.묻힌 식

JSX 는 태그 내에서 꽃괄호 구문 ({ }) 을 통해 식을 삽입하는 것을 허가합니다:

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

(Embedding Expressions in JSX 에서 발췌)

TypeScript 도 서포트하며, 묻힌 식에 대해 타입 체크를 수행할 수 있습니다:

const a = <div>
  {/* 에러 The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. */}
  {["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>

六.React 와의 결합

[React 타입 정의](https://www.npmjs.com/package/ @types/react) 를 도입한 후, Props 의 타입을 간단히 기술할 수 있습니다:

interface WelcomeProps {
  name: string;
}
// Props 의 타입을 첫 번째 타입 파라미터로서 건넴
class WelcomeClass extends React.Component<WelcomeProps, {}> {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
// 에러 Property 'name' is missing in type '{}' but required in type 'Readonly<WelcomeProps>'.
let errorCase = <WelcomeClass />;
let correctCase = <WelcomeClass name="Lily" />;

P.S. 타입 파라미터 및 제네릭의 더 많은 정보는, [二。타입 변수](/articles/제네릭-typescript 노트 6/#articleHeader3) 참조

팩토리 함수

React 모드 (--jsx react) 에서는, 구체적으로 사용하는 JSX 요소 팩토리 메서드를 구성할 수 있으며, 2 가지 방법이 있습니다:

  • --jsxFactory 옵션: 프로젝트 레벨 구성

  • 인라인 @jsx 코멘트 지시: 파일 레벨 구성

디폴트는 --jsxFactory "React.createElement" 로, JSX 태그를 팩토리 메서드 호출로 변환합니다:

const div = <div />;
// 컴파일 결과
var div = React.createElement("div", null);

Preact 내에서는 대응하는 JSX 요소 팩토리 메서드는 h 입니다:

/* @jsx preact.h */
import * as preact from "preact";
<div />;
// 또는
/* @jsx h */
import { h } from "preact";
<div />;

P.S. 주의, @jsx 코멘트 지시는 반드시 파일의 선두 행에 나타날 필요가 있으며,其余의 위치에서는 무효입니다

컴파일 결과는 각각:

/* @jsx preact.h */
var preact = require("preact");
preact.h("div", null);
// 또는
/* @jsx h */
var preact_1 = require("preact");
preact_1.h("div", null);

P.S. 게다가, 팩토리 메서드 구성은 JSX 네임스페이스의 검색에도 영향줍니다. 예를 들어 디폴트 --jsxFactory React.createElement 의 경우, 우선적으로 React.JSX 를 검색하고, 다음에 글로벌 JSX 네임스페이스를 봅니다. --jsxFactory h 를 지정하면, 우선적으로 h.JSX 를 검색합니다

七.정리

TypeScript 중 JSX 의 타입 서포트는 요소 타입, 속성 타입, 결과 타입의 3 부분으로 나뉘며, 아래 그림과 같습니다:

tsx

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성