TypeScript에 입문하는 React 개발자를 위한 치트시트(Cheetsheets)
웹 다큐먼트 | 영어판 | 프로젝트에 기여하기 | 질문하기
👋 본 리포지토리는 @ryan_kim_kr에 의해 관리되고 있습니다. 개발자님이 React와 함께 TypeScript를 사용해보고자 하시다니 정말 기쁜 소식이군요! 잘못된 부분이 발견되어 수정이 필요하거나 누락된 부분이 있다면 개선 되어야 할 사항을 이슈 등록해 주시기 바랍니다. 👍
- 기초 치트시트(The Basic Cheatsheet)는 React 개발자가 React app에서 TS 사용을 시작하는 것에 도움을 주기 위한 내용이 주를 이룹니다.
- 모범 사례(Best Practices)라고 여겨지는, 복사 + 붙여넣기 가능한 예시
- 기본적인 TS Types 사용법과 설정 방법
- 자주 묻는 질문(FAQ)에 대한 답변
- Generic type logic은 깊이 다루지 않습니다. 그 대신, 초심자들을 위해 간단한 트러블슈팅 기술들을 소개합니다.
- 기초 치트시트는 개발자가 TypeScript에 대해 너무 많은 공부를 하지 않고서도 시간 효율적으로 React 개발에 TypeScript를 빠르게 사용할 수 있도록 돕는 데 그 목적이 있습니다.
- 고급 치트시트(The Advanced Cheatsheet)는 재사용 가능한 type utilities/functions/render prop/higher order copmonents 또는 TS+React 라이브러리를 작성하고자 하는 개발자를 위해 generic types의 고급 사용법에 대한 이해를 돕습니다.
- 전문적인 개발자들을 위한 다양한 팁과 요령들을 소개합니다.
- DefinitelyTyped에 기여하기 위한 조언을 드립니다.
- 고급 치트시트는 개발자가 TypeScript를 최대한 활용할 수 있도록 돕는 데 그 목적이 있습니다.
- 마이그레이팅 치트시트(The Migrating Cheatsheet)는 대규모 코드베이스를 JS 또는 Flow에서 TypsScript로 점진적으로 마이그레이션 하는 것에 대한 경험자의 조언을 얻는데 도움을 줍니다.
- 우리는 여러분이 마이그레이션을 하도록 설득하려는 것이 아니며, 이미 그렇게 하고자 결정한 사람들을 돕고자 합니다.
⚠️ 이 치트시트는 새롭게 만들어진 치트시트 입니다. 따라서 도움을 주고자 하는 모든 분들을 환영합니다.
- HOC 치트시트(The HOC Cheatsheet)는 예시와 함께 HOC를 작성하는 방법을 알려줍니다.
- Generics에 대한 이해가 선행되어야 합니다.
⚠️ 이 치트시트는 새롭게 만들어진 치트시트 입니다. 따라서 도움을 주고자 하는 모든 분들을 환영합니다.
목차 확장하기
- React에 대한 충분한 이해
- TypeScript Types주제에 대한 이해 (2ality's guide를 알고있으면 문서를 이해하는데 도움이 됩니다. 만약 TypeScript를 처음 접하는 분이라면,chibicode’s tutorial를 참고해 보세요.)
- the TypeScript section in the official React docs 읽기
- the React section of the new TypeScript playground 읽기 (선택사항: the playground's Example Section의 40+ examples 단계를 수행해 보기)
이 가이드는 독자가 가장 최신 버전의 TypeScript와 React를 사용한다고 가정합니다. 이전 버전에 대한 사항은 확장 가능한 <details>
태그로 확인 가능합니다.
- 리펙토링 보조 https://marketplace.visualstudio.com/items?itemName=paulshen.paul-typescript-toolkit
- R+TS Code Snippets (여러가지 확장 프로그램이 있습니다...)
- TypeScript 공식 확장프로그램 https://code.visualstudio.com/docs/languages/typescript
Cloud setups:
- TypeScript Playground with React는 코드를 실행하지 않고 Types를 디버깅만 하는 경우 사용할 수 있습니다.
- CodeSandbox - cloud IDE, 매우 빠른 부팅 속도를 가집니다.
- Stackblitz - cloud IDE, 매우 빠른 부팅 속도를 가집니다.
Local dev setups:
- Next.js:
npx create-next-app -e with-typescript
명령어는 새로운 NextJS 프로젝트를 현재 폴더에 생성합니다. - Create React App:
npx create-react-app name-of-app --template typescript
명령어는 새로운 NextJS 프로젝트를 새로운 폴더에 생성합니다. - Vite:
npm create vite@latest my-react-ts-app -- --template react-ts
- Meteor:
meteor create --typescript name-of-my-new-typescript-app
- Ignite for React Native:
ignite new myapp
- TSDX:
npx tsdx create mylib
명령어는 React+TS 라이브러리 를 생성합니다. (in future: TurboRepo)
다른 도구들
아직 보완이 필요하지만 확인해 볼 만한 가치가 있는 도구들:
- Snowpack:
npx create-snowpack-app my-app --template app-template-react-typescript
- Docusaurus v2 with TypeScript Support
- Parcel
- JP Morgan's
modular
: CRA + TS + Yarn Workspaces toolkit.yarn create modular-react-app <project-name>
Manual setup:
- Basarat's guide는 React + TypeScript + Webpack + Babel 을 수동으로 설정 할 경우 사용할 수 있습니다.
- 특히,
@types/react
와@types/react-dom
가 설치되어 있는지 확인이 필요합니다. (익숙하지 않은 내용이라면 DefinitelyTyped project 에 대해 더 알아보세요.) - 또한 많은 React + TypeScript bolierplates들이 있습니다. 우리의 다른 리소스 리스트를 확인해주세요.
아래의 7부로 구성된 "React Typescript Course" 비디오 시리즈를 통해 TypeScript with React에 대한 소개를 들을 수 있습니다.
함수 컴포넌트는 props
를 매개변수로 받고 JSX element를 반환하는 일반적인 함수로 작성될 수 있습니다.
// props의 타입 정의 - 더 많은 예시는 "컴포넌트 Props 타이핑"에서 확인할 수 있습니다.
type AppProps = {
message: string;
}; /* export 한다면 consumer가 extend할 수 있도록 `interface`를 사용하세요. */
// 함수 컴포넌트를 정의할 수 있는 가장 쉬운 방법; return type은 추론됩니다.
const App = ({ message }: AppProps) => <div>{message}</div>;
// 실수로 다른 타입을 반환하였을 때 에러가 raise 되도록 return type을 명시할 수 있습니다.
const App = ({ message }: AppProps): JSX.Element => <div>{message}</div>;
// type 선언을 함수 컴포넌트 선언에 포함시킬 수 있습니다.;이 방법은 prop types에 이름을 붙이지 않아도 되지만 코드가 반복됩니다.
const App = ({ message }: { message: string }) => <div>{message}</div>;
Tip: type destructure 선언을 위해 Paul Shen's VS Code Extension를 사용할 수도 있습니다. (keyboard shortcut을 추가 하세요.)
React.FC
가 권장되지 않는 이유는 무엇일까요? React.FunctionComponent
/React.VoidFunctionComponent
는 어떤가요?
React+TypeScript codebases에서 다음 보았을 수 있습니다.
const App: React.FunctionComponent<{ message: string }> = ({ message }) => <div>{message}</div>;
하지만, 현재 React.FunctionComponent
(또는 간략하게 써서 React.FC
)는 권장되지 않는다는 것에 대부분의 사람들이 동의합니다. 물론 이 주제에 대한 미묘한 의견 차이가 있을 수는 있지만, 만약 이 의견에 동의하고 React.FC
를 당신의 코드베이스에서 제거하고 싶다면, 이 jscodeshift codemond를 사용할 수 있습니다.
"일반적인 함수" 버전과의 차이점들은 다음과 같습니다.
-
React.FunctionComponent
는 return type을 명시적으로 밝힙니다. 하지만 일반적인 함수 버전은 암시적입니다(또는 추가적인 어노테이션(annotation)이 필요합니다). -
displayName
,propTypes
, 그리고defaultProps
와 같은 static properties를 위한 자동완성(autocomplete)과 타입 체크(Typechecking)를 지원합니다.React.FunctionComponent
와 함께defaultProps
을 사용하는데 몇 가지 알려진 문제가 있습니다. 문제에 대한 자세한 내용을 확인하세요. 우리는 개발자님이 찾아볼 수 있는 별개의defaultProps
섹션을 제공하고 있습니다.
-
React 18 type 업데이트 이전에는,
React.FunctionComponent
이children
에 대한 암시적인 정의(implicit definition)를 제공했었습니다. 이것은 열띤 토론 과정을 거쳤고 결과적으로React.FC
가 Create React App TypeScript template에서 제거된 이유 중 하나가 되었습니다.
// React 18 types 이전
const Title: React.FunctionComponent<{ title: string }> = ({ children, title }) => (
<div title={title}>{children}</div>
);
(Deprecated)React.VoidFunctionComponent
또는 React.VFC
사용하기
@types/react 16.9.48에서, React.VoidFunctionComponent
또는 React.VFC
type은 children
을 명시적으로 타이핑(typing) 하기 위해 추가되었습니다.
하지만, React.VFC
와 React.VoidFunctionComponent
는 React 18 (DefinitelyTyped/DefinitelyTyped#59882) 에서 더이상 사용되지 않게 되었습니다(deprecated). 따라서 이 임시방편은 React 18+ 에서 더이상 권장되지 않습니다.
일반적인 함수 컴포넌트나 React.FC
를 사용해 주세요.
type Props = { foo: string };
// 지금은 괜찮지만, 미래에는 에러를 발생시킬 것입니다.
const FunctionComponent: React.FunctionComponent<Props> = ({ foo, children }: Props) => {
return (
<div>
{foo} {children}
</div>
); // OK
};
// 지금은 에러를 발생시키고, 미래에는 더이상 사용되지 않을것입니다.(Deprecated)
const VoidFunctionComponent: React.VoidFunctionComponent<Props> = ({ foo, children }) => {
return (
<div>
{foo}
{children}
</div>
);
};
- 미래에는, props를 자동으로
readonly
라고 표시할 수도 있습니다. 하지만, props 객체가 파라미터 리스트에서 destructure 된다면, 이것은 의미없는 행동 입니다.
대부분의 경우에는 어떤 syntax를 사용하던지 큰 차이가 없지만, React.FunctionComponent
의 보다 명시적인 특성을 선호하는 것이 좋을것입니다.
주의해야 할 사항
다음의 패턴은 지원되지 않습니다. :
조건부 렌더링(conditional rendering)
const MyConditionalComponent = ({ shouldRender = false }) => (shouldRender ? <div /> : false); // JS 에서도 이렇게 하지 마십시오.
const el = <MyConditionalComponent />; // 에러를 throw 합니다.
이 패턴이 지원되지 않는 이유는 컴파일러의 한계 때문입니다. 함수 컴포넌트는 JSX expression 또는 null
이외의 다른 어떤 것도 반환할 수 없습니다. 반환할 수 없는 것이 반환된다면 해당 타입은 Element
에 할당될 수 없다는 에러 메세지를 보게될 것입니다. ("{the other type} is not assignable to Element
.")
Array.fill
const MyArrayComponent = () => Array(5).fill(<div />);
const el2 = <MyArrayComponent />; // throws an error
아쉽게도 함수의 타입을 annotate 하는 것은 아무런 도움이 되지 않을것입니다. React가 지원하는 다른 특별한 타입(exotic type)을 반환하고자 한다면 타입 표명(type assertion)을 수행해야 합니다. :
const MyArrayComponent = () => Array(5).fill(<div />) as any as JSX.Element;
Hook은 @types/react
v16.8 이상부터 지원됩니다.
타입 추론(Type inference)은 간단한 값들에 잘 작동합니다:
const [state, setState] = useState(false);
// `state` 는 boolean 으로 추론됩니다.
// `setState` 는 boolean 값 만을 받습니다.
타입 추론에 복잡한 타입을 사용해야 한다면 추론된 타입(Inferred Types) 사용하기 도 확인해보세요.
하지만 많은 hook 들은 null 같은 값를 디폴트 값으로 초기화 하기 때문에 어떻게 타입을 지정하는지 궁금할 수 있습니다. 명시적으로 타입을 선언하고, union type을 사용하세요.:
const [user, setUser] = useState<User | null>(null);
// later...
setUser(newUser);
만약 useState설정 직후에 state가 초기화되고 그 이후에 항상 값을 가진다면, 타입 표명(type assertions)을 사용할 수도 있습니다.
const [user, setUser] = useState<User>({} as User);
// later...
setUser(newUser);
이 방법은 일시적으로 타입스크립트 컴파일러에게 {}
가 User
의 type이라고 "거짓말" 합니다. 그 후에 user
state를 설정하여야 합니다. 그렇지 않으면 나머지 코드가 user
는 User
타입이라는 사실에 의존고 이것은 런타입 에러로 이어질 수 있습니다.
Reducer actions를 위해 Discriminated Unions를 사용할 수 있습니다. reducer의 return type을 정의하는 것을 잊지 마세요. 그렇지 않으면 타입스크립트가 return type을 추론할 것입니다.
import { useReducer } from "react";
const initialState = { count: 0 };
type ACTIONTYPE = { type: "increment"; payload: number } | { type: "decrement"; payload: string };
function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case "increment":
return { count: state.count + action.payload };
case "decrement":
return { count: state.count - Number(action.payload) };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement", payload: "5" })}>-</button>
<button onClick={() => dispatch({ type: "increment", payload: 5 })}>+</button>
</>
);
}
Redux
에서 Reducer
와 함께 사용하기
Reducer funciton을 작성하기 위해 redux를 사용하는 경우, return type을 처리하는 Reducer<State, Action>
형식의 편리한 helper를 사용할 수 있습니다.
위의 reducer example은 다음과 같이 바뀔 수 있습니다. :
import { Reducer } from 'redux';
export function reducer: Reducer<AppState, Action>() {}
userEffect
와 userLayoutEffect
둘 다 side effect를 수행하기 위해 사용되고 선택적으로 cleanup function을 반환합니다. 이것은 만약 이 hook들이 반환 값을 처리하지 않는다면, type이 필요 없다는 뜻입니다. useEffect
를 사용할 때, 함수 또는 undefined
이외의 다른 것을 반환하지 않도록 주의하세요. 그렇지 않으면 TypeScript와 React는 당신에게 비명을 지를것입니다. Arros functions를 사용한다면 이 문제는 다소 파악하기 어려울 수 있습니다. :
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(
() =>
setTimeout(() => {
/* do stuff */
}, timerMs),
[timerMs]
);
// 나쁜 예시! setTimeout은 암묵적으로 숫자를 반환하고 있습니다.
// arrow function의 body가 중괄호로 감싸지지 않았기 때문입니다.
return null;
}
위 예시에 대한 해결책
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
useEffect(() => {
setTimeout(() => {
/* do stuff */
}, timerMs);
}, [timerMs]);
// 더 나은 방법; 확실하게 undefined를 반환하기 위해서 void keyword를 사용하세요.
return null;
}
TypeScript에서 useRef
는 type argument가 초기 값을 완전히 포함(cover)하는지 아닌지에 따라read-only또는 mutable 둘 중 하나를 반환합니다. 각자의 use case에 맞는 것을 선택하세요.
DOM element에 접근하기 위해서는: element type 만을 argument로 넘겨주고 null
을 초기 값으로 사용하세요. 이 경우에, 반환되는 reference는 React에 의해 관리되는 read-only .current
를 가질 것입니다. TypeScript는 이 ref를 element의 ref
prop으로 전달 받기를 기대합니다. :
function Foo() {
// - 가능한 상세하게 작성하세요. 예를들면, HTMLDivElement는 HTMLElement보다 더 좋고,
// Element보다는 훨신 더 좋은 선택입니다.
// - 기술적으로 말하자면, 이것은 RefObject<HTMLDivElement>를 반환합니다.
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// ref.current가 null일 수 있다는 것을 주의하세요.
// 이것은 당신이 조건에 따라서 ref된(ref-ed) element를 render하거나
// 할당하는 것을 잊을 수 있기 때문에 예측할 수 있는 현상입니다.
if (!divRef.current) throw Error("divRef is not assigned");
// 이제 divRef.current는 확실하게 HTMLDivElement 입니다.
doSomethingWith(divRef.current);
});
// React가 당신을 위해 ref를 관리할 수있도록 element에게 ref를 전달해 주세요.
return <div ref={divRef}>etc</div>;
}
만약 divRef.current
가 절대로 null이 아닐것이라는 것을 확신한다면, non-null assertion operator !
을 사용하는 것도 가능합니다. :
const divRef = useRef<HTMLDivElement>(null!);
// 나중에... 이것이 null 인지 확인할 필요가 없습니다.
doSomethingWith(divRef.current);
당신이 type safety가 보장된다고 미리 가정하고 코드를 작성한다는 것에 주의해야 합니다. 렌더링 과정에서 ref를 element에 할당하는 것을 잊거나, ref된(ref-ed) element가 조건부 렌더링 된다면 runtime error가 발생할 것입니다.
Tip: 어떤 HTMLElement
를 사용할지 선택하기
Ref는 명시성(specificity)을 필요로 합니다. 즉, HTMLElement
만을 명시하는 것은 충분하지 않다는 말입니다. 만약 당신이 필요한 element type의 이름을 모른다면, lib.dom.ts에서 확인하거나 의도적으로 type error를 발생시키고 language service가 type의 이름을 알려주도록 할 수 있습니다.
mutable value를 가지기 위해서는: 원하는 type을 사용하고 초기 값이 완전히 해당 type에 속하는지 확인하세요.
function Foo() {
// 기술적으로 말하자면, 이것은 MutableRefObject<number | null>을 반환합니다.
const intervalRef = useRef<number | null>(null);
// 당신이 직접 ref를 관리합니다. (이것이 MutableRefObject라고 불리는 이유이죠.)
useEffect(() => {
intervalRef.current = setInterval(...);
return () => clearInterval(intervalRef.current);
}, []);
// ref는 element의 "ref" prop으로 전달되지 않습니다.
return <button onClick={/* clearInterval the ref */}>Cancel timer</button>;
}
해당 Stackoverflow answer에 따르면 다음과 같습니다.:
// Countdown.tsx
// forwardRef로 전달될 handle type을 정의합니다
export type CountdownHandle = {
start: () => void;
};
type CountdownProps = {};
const Countdown = forwardRef<CountdownHandle, CountdownProps>((props, ref) => {
useImperativeHandle(ref, () => ({
// start() 는 여기서 타입 추론(type inference) 됩니다
start() {
alert("Start");
},
}));
return <div>Countdown</div>;
});
// 이 컴포는트는 Countdown 컴포넌트를 사용합니다
import Countdown, { CountdownHandle } from "./Countdown.tsx";
function App() {
const countdownEl = useRef<CountdownHandle>(null);
useEffect(() => {
if (countdownEl.current) {
// start()는 여기서도 타입 추론(type inference) 됩니다.
countdownEl.current.start();
}
}, []);
return <Countdown ref={countdownEl} />;
}
만약 Custom Hook에서 array를 return한다면, array의 각 위치에서 각기 다른 type을 가지기를 원하겠지만 TypeScript는 union type으로 추론할 것이기 때문에 타입 추론을 피하고 싶을 것입니다. 이러한 상황에서 TS 3.4 const assertions을 사용할 수 있습니다.
import { useState } from "react";
export function useLoading() {
const [isLoading, setState] = useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as const; // (boolean | typeof load)[]이 아닌 [boolean, typeof load]으로 추론합니다.
}
TypeScript Playground에서 확인해 보기
이런 방법으로, destructure했을 때 destructureg한 위치에 따라 올바른 type을 얻을 수 있습니다.
대안: tuple return type을 표명하기(assert)
만약 const assertions이 사용하기 어렵다면, 함수 return type을 표명(assert)하거나 정의할 수 있습니다.
import { useState } from "react";
export function useLoading() {
const [isLoading, setState] = useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load] as [boolean, (aPromise: Promise<any>) => Promise<any>];
}
많은 custom hooks을 작성한다면, 자동으로 tuples의 타입을 명시해주는 helper도 큰 도움이 될 수 있습니다.
function tuplify<T extends any[]>(...elements: T) {
return elements;
}
function useArray() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return [numberValue, functionValue]; // type is (number | (() => void))[]
}
function useTuple() {
const numberValue = useRef(3).current;
const functionValue = useRef(() => {}).current;
return tuplify(numberValue, functionValue); // type is [number, () => void]
}
하지만 React team은 두개 이상의 값을 return하는 custom hook은 tuple 대신 적절한 object를 사용하는 것을 권장한다는 것에 주의하세요.
- https://medium.com/@jrwebdev/react-hooks-in-typescript-88fce7001d0d
- https://fettblog.eu/typescript-react/hooks/#useref
만약 React Hooks library를 작성하고 있다면, 사용자들이 사용할 수 있도록 types를 export 해야 한다는 것을 잊지 마세요.
- https://github.com/mweststrate/use-st8
- https://github.com/palmerhq/the-platform
- https://github.com/sw-yx/hooks
TypeScript에서 React.Component
는 generic type (aka React.Component<PropType, StateType>
)입니다. 따라서 React.Component
에 prop과 state type parameter를 전달해야 합니다. :
type MyProps = {
// `interface`를 사용하는 것도 괜찮습니다
message: string;
};
type MyState = {
count: number; // 이런 식으로
};
class App extends React.Component<MyProps, MyState> {
state: MyState = {
// 더 나은 타입 추론을 위해 선택적으로 작성한 두 번째 annotation
count: 0,
};
render() {
return (
<div>
{this.props.message} {this.state.count}
</div>
);
}
}
재사용하기 위해 이러한 types/interfaces를 export/import/extend 할 수 있다는 것을 잊지 마세요.
왜 state
를 두 번 annotate 할까요?
반드시 state
class property에 annotate할 필요는 없지만, 이렇게 하면 this.state
에 접근하거나 state를 초기화 할 때 더 나은 타입 추론을 가능하게 합니다.
그 이유는 두 개의 annotation은 서로 다른 방식으로 동작하기 때문입니다. 두 번째 generic type parameter는 this.setState()
가 올바르게 동작하도록 해줍니다. 왜냐하면 이 메소드는 base class에서 오기 때문입니다. 하지만 컴포넌트 내에서 state
를 초기화 하는 것은 base implementation을 override하기 때문에 컴파일러에게 사실상 다른 작업을 하고 있지 않다는 것을 알려줘야 합니다. (=컴파일러에게 사실상 같은 작업을 하고 있다는 것을 알려줘야 합니다.)
readonly
는 필요 없다
종종 샘플 코드에 props와 state가 변할 수 없다고 표시하기 위해 readonly
를 포합하는 것을 볼 수 있습니다.
type MyProps = {
readonly message: string;
};
type MyState = {
readonly count: number;
};
React.Component<P,S>
가 이미 props와 state가 변할 수 없다고 표시했기 때문에 추가적으로 readonly표시를 할 필요가 없습니다. (PR 과 discussion을 확인하세요!)
Class Methods: 원래 하던데로 하되, 당신의 함수를 위한 모든 arguments는 type이 있어야 한다는 것만 기억하세요.
class App extends React.Component<{ message: string }, { count: number }> {
state = { count: 0 };
render() {
return (
<div onClick={() => this.increment(1)}>
{this.props.message} {this.state.count}
</div>
);
}
increment = (amt: number) => {
// 이런 식으로
this.setState((state) => ({
count: state.count + amt,
}));
};
}
TypeScript Playground에서 확인해 보기
Class Properties: 만약 나중에 사용하기 위해 class properties를 선언한다면, state
와 같이 선언하되 할당은 하지 안습니다.
class App extends React.Component<{
message: string;
}> {
pointer: number; // 이런 식으로
componentDidMount() {
this.pointer = 3;
}
render() {
return (
<div>
{this.props.message} and {this.pointer}
</div>
);
}
}
TypeScript Playground에서 확인해 보기
getDerivedStateFromProps
를 사용하기 전에, documentation과 You Probably Don't Need Derived State를 읽어보세요. Derived State는 memoization을 설정한는 것을 도울 수 있는 hooks을 사용하여 구현될 수 있습니다.
다음은 getDerivedStateFromProps
를 annotate할 수 있는 몇 가지 방법입니다.
- 만약 derived state의 type을 명시적으로 설정했고,
getDerivedStateFromProps
의 return 값이 설정한 type을 준수하는지 알고싶은 경우
class Comp extends React.Component<Props, State> {
static getDerivedStateFromProps(props: Props, state: State): Partial<State> | null {
//
}
}
- 함수의 return 값이 state를 결정하도록 하고싶은 경우
class Comp extends React.Component<Props, ReturnType<typeof Comp["getDerivedStateFromProps"]>> {
static getDerivedStateFromProps(props: Props) {}
}
- 다른 state fields와 derived state 그리고 memoization을 원할 경우
type CustomValue = any;
interface Props {
propA: CustomValue;
}
interface DefinedState {
otherStateField: string;
}
type State = DefinedState & ReturnType<typeof transformPropsToState>;
function transformPropsToState(props: Props) {
return {
savedPropA: props.propA, // save for memoization
derivedState: props.propA,
};
}
class Comp extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
otherStateField: "123",
...transformPropsToState(props),
};
}
static getDerivedStateFromProps(props: Props, state: State) {
if (isEqual(props.propA, state.savedPropA)) return null;
return transformPropsToState(props);
}
}
TypeScript Playground에서 확인해 보기
이 트윗에 따르면, defaultProps는 deprecate 될 것입니다.. 다음의 토론을 확인해 보세요.:
- Original tweet
- 이 article에서 더 많은 정보를 얻을 수 있습니다.
object default value를 사용하는 것이 통상적으로 합의된 내용입니다.
Function Components:
type GreetProps = { age?: number };
const Greet = ({ age = 21 }: GreetProps) => // etc
Class Components:
type GreetProps = {
age?: number;
};
class Greet extends React.Component<GreetProps> {
render() {
const { age = 21 } = this.props;
/*...*/
}
}
let el = <Greet age={3} />;
TypeScript 3.0+에서 타입 추론의 성능이 매우 많이 발전되었습니다. 하지만 여전히 몇몇의 edge case 들이 문제가 되기는 합니다..
Function Components
// 쉬운 방법으로 typeof를 사용할 수 있습니다.; hoist되는 것에 주의하세요!
// DefaultProps을 선언할 수도 있습니다.
// e.g. https://github.com/typescript-cheatsheets/react/issues/415#issuecomment-841223219
type GreetProps = { age: number } & typeof defaultProps;
const defaultProps = {
age: 21,
};
const Greet = (props: GreetProps) => {
// etc
};
Greet.defaultProps = defaultProps;
Class components의 경우, 타이핑을 위한 두 가지 방법 (including using the Pick
utility type)이 있지만, props definition을 거꾸로 수행("reverse") 하는 것을 추천합니다.
type GreetProps = typeof Greet.defaultProps & {
age: number;
};
class Greet extends React.Component<GreetProps> {
static defaultProps = {
age: 21,
};
/*...*/
}
// Type-checks! type assertion이 필요 없음!
let el = <Greet age={3} />;
라이브러리 작성자를 위한JSX.LibraryManagedAttributes
뉘앙스
위에서 소개된 구현은 앱 개발자들이 사용하는데 아무런 문제가 없습니다. 하지만 다른 사람들이 사용(consume)할 수 있도록 GreetProps
를 export 하고싶은 경우도 있습니다. 여기서 GreetProps
가 정의되는 방법이 문제가 됩니다. age
는 꼭 필요하지 않을 때에도 defaultProps
때문에 필수적인 props가 됩니다.
GreetProps
는 당신의 컴포넌트를 위한 내부적인 규칙(컴포넌트가 구현하는 것)이지, 외부적인 것이 아닙니다. 따라서 export를 위한 type을 따로 만들 거나, JSX.LibraryManagedAttributes
utility를 사용할 수도 있습니다.
// internal contract(내부적인 규칙), export 되어서는 안된다
type GreetProps = {
age: number;
};
class Greet extends Component<GreetProps> {
static defaultProps = { age: 21 };
}
// external contract(외부적인 규칙)
export type ApparentGreetProps = JSX.LibraryManagedAttributes<typeof Greet, GreetProps>;
이렇게 하면 코드가 잘 실행되지만, ApparentGreetProps
를 사용하는게 다소 번거로울 수 있습니다. 아래에서 설명하는 ComponentProps
utility로 boilerplate를 간단하게 만들 수 있습니다.
defaultProps
가 있는 컴포넌트는 실제로는 그렇지 않지만 필수적인 props를 가지는 것처럼 보일 수 있습니다.
다음과 같은 작업을 하고싶다면,
interface IProps {
name: string;
}
const defaultProps = {
age: 25,
};
const GreetComponent = ({ name, age }: IProps & typeof defaultProps) => (
<div>{`Hello, my name is ${name}, ${age}`}</div>
);
GreetComponent.defaultProps = defaultProps;
const TestComponent = (props: React.ComponentProps<typeof GreetComponent>) => {
return <h1 />;
};
// 'age' property는 '{ name: string; }'에 없지만, '{ age: number; } type에서는 필수적인 property 입니다.
const el = <TestComponent name="foo" />;
JSX.LibraryManagedAttributes
를 적용하는 유틸리티를 정의합니다.
type ComponentProps<T> = T extends React.ComponentType<infer P> | React.Component<infer P>
? JSX.LibraryManagedAttributes<T, P>
: never;
const TestComponent = (props: ComponentProps<typeof GreetComponent>) => {
return <h1 />;
};
// No error
const el = <TestComponent name="foo" />;
React.FC
는 왜 defaultProps
이 동작하지 못하게 만들까요?
다음의 토론을 확인해 보세요.:
- https://medium.com/@martin_hotell/10-typescript-pro-tips-patterns-with-or-without-react-5799488d6680
- DefinitelyTyped/DefinitelyTyped#30695
- typescript-cheatsheets/react#87
이건 현재 상태일 뿐이고, 추후에는 올바르게 수정될 것입니다.
TypeScript 2.9 and earlier
TypeScript 2.9와 그 이전 버전에서는 이문제를 해결하는 방법이 다양합니다. 하지만 다음 방법이 여태까지 확인한 방법 중 가장 좋은 방법입니다.
type Props = Required<typeof MyComponent.defaultProps> & {
/* 추가적인 props */
};
export class MyComponent extends React.Component<Props> {
static defaultProps = {
foo: "foo",
};
}
이전에는 TypeScript의 Partial type
기능을 사용하는 것이 권장 사항이었는데, 이는 현재 인터페이스가 래핑된 인터페이스에에서 부분적인 버전을 충족한다는 것을 의미합니다. 이런 방법으로 type을 변경하지 않고 defaultProps를 확장할 수 있습니다.
interface IMyComponentProps {
firstProp?: string;
secondProp: IPerson[];
}
export class MyComponent extends React.Component<IMyComponentProps> {
public static defaultProps: Partial<IMyComponentProps> = {
firstProp: "default",
};
}
이 접근 방법의 문제점은 JSX.LibraryManagedAttributes
로 작동하는 타입 추론에 복잡한 이슈를 발생시킨다는 것입니다. 기본적으로 컴파일러는 해당 컴포넌트로 JSX expression을 생성할 때 모든 props가 선택사항(optional)이라고 생각합니다.