개요
리액트는 메타 코드 기반에 더 많은 구조를 제공하고 훨씬 더 나은 확장을 가능하게 하기 위해 만들었다.
React는 Facebook에서 사용하고 있으며 오픈 소스로 만들었다.
최근 React는 프론트 엔드를 구축하기 위한 기술 중 가장 많이 선호하고 있다.
그것은 작은 프로젝트나 대형 프로젝트에서도 프론트엔드를 만들기 위해 함께 구성할 수 있는 재사용 가능성이 높은 구성 요소이다.
TypeScript는 개발자들이 대규모 자바스크립트 기반의 개발을 더 쉽게 할 수 있도록 마이크로소프트에 의해 제작되었습니다
프로그램들이 그것은 풍부한 종류의 시스템을 제공하는 자바스크립트의 세트이다.
개발자들은 버그를 조기에 발견하고 강력하게 코드를 탐색하고 리팩터하기 위해 도구를 만들 수 있습니다.
설치 프로그램
Google Chrome : https://www. google.com/chrome/
Node.js and npm: https://nodejs.org/en/download/
Visual Studio Code: https://code.visualstudio.com/
React 18.0
TypeScript 4.7
소스 파일 :
https://github.com/PacktPublishing/Learn-React-with-TypeScript-2nd-Edition
타입스크립트의 Never 타입
타입스크립트에서 never 타입은 값의 공집합이다. 사실 또 다른 인기 자바스크립트 타입 시스템인 Flow에서 never 타입은 empty 타입과 같다.
집합에 어떤 값도 없기 때문에, never 타입은 any 타입의 값을 포함해 어떤 값도 가질 수 없다. 그래서 never 타입은 때때로 점유할 수 없는 또는 바닥 타입이라고 불린다.
declare const any: any
const never: never = any // ❌ 'any' 타입은 'never'타입에 할당할 수 없다.
바닥 타입은 타입스크립트 핸드북이 never 타입을 정의하는 방식이다. 하위 타입 지정을 이해하는 데 사용하는 멘탈 모델인 타입 계층 트리에 never를 배치하면 더 말이 된다는 것을 알았다.
다음 논리적 질문은 "never 타입이 왜 필요할까?"이다.
never 타입이 왜 필요할까?
숫자 체계에 아무것도 없는 양을 나타내는 0처럼 문자 체계에도 불가능을 나타내는 타입이 필요하다.
"불가능"이라는 단어 자체는 모호하다. 타입스크립트에서는 "불가능"을 아래와 같이 다양한 방법으로 나타내고 있다.
- 값을 포함할 수 없는 빈 타입
- 제네릭과 함수에서 허용되지 않는 매개변수
- 호환되지 않는 타입들의 교차 타입
- 빈 합집합(무의 합집합)
- 실행이 끝날 때 호출자에게 제어를 반환하지 않는 함수의 반환 타입
- 예) Node의 process.exit
- void는 호출자에게 함수가 유용한 것을 반환하지 않는다는 것이므로 혼동하지 않도록 한다.
- 절대로 도달할수 없을 esle 분기의 조건 타입
- 거부된 프로미스에서 처리된 값의 타입
-
const p = Promise.reject('foo') // const p: Promise<never>
유니언/교차 타입과 never의 동작
숫자 0이 덧셈과 곱셈에 작용 방법과 유사하듯이, never타입은 유니언/교차 타입에서 특별한 속성을 지닌다.
- 숫자에 0을 더하면 동일한 숫자가 나오는 것과 같이, never 타입은 유니언 타입에서 없어진다.
-
type Res = never | string // string
- 숫자에 0을 곱하면 0이 나오는 것과 같이, never 타입은 교차 타입을 덮어쓴다.
-
type Res = never & string // never
never 타입의 이런 동작과 특징은 앞으로 보게 될 가장 중요한 사용 사례의 기초를 마련한다.
never 타입은 어떻게 쓸까?
never 타입을 많이 사용하지 않을 수 있지만, 아래와 같이 적절한 사용 사례가 많이 있다.
허용할 수 없는 함수 매개변수에 제한을 가한다.
never 타입을 이용해서 다양한 사용 사례에 놓인 함수에 제안을 걸 수 있다.
switch, if-else 문의 모든 상황을 보장한다.
함수가 단 하나의 never 타입 인수만을 받을 수 있는 경우, (타입스크립트 컴파일러가 오류를 발생하지 않고는) 해당 함수를 never 타입 이외의 값으로 호출할 수 없다.
function fn(input: never) {}
// 오직 `never` 만 받는다.
declare let myNever: never
fn(myNever) // ✅
// 아무 값이나 전달하거나 아무 값도 전달하지 않으면 타입 에러 발생
fn() // ❌ 인자 'input'에 아무 값도 주어지지 않음
fn(1) // ❌ 'number' 타입은 'never' 타입에 할당할 수 없음
fn('foo') // ❌ 'string' 타입은 'never' 타입에 할당할 수 없음
// `any`도 통과할 수 없다.
declare let myAny: any
fn(myAny) // ❌ 'any' 타입은 'never' 타입에 할당할 수 없음
이런 함수를 이용하면 switch, if-else 문의 모든 상황을 보장할 수 있다. 이를 기본 케이스(default case)로 이용하면 남아있는 것은 never 타입이어야 하기 때문에 모든 상황에 대처하는 것을 보장할 수 있다.
function unknownColor(x: never): never {
throw new Error("unknown color");
}
type Color = 'red' | 'green' | 'blue'
function getColorName(c: Color): string {
switch(c) {
case 'red':
return 'is red';
case 'green':
return 'is green';
default:
return unknownColor(c); // 'string' 타입은 'never' 타입에 할당할 수 없음
}
}
타이핑을 부분적으로 허용하지 않는다.
VariantA 또는 VariantB 타입의 매개변수를 받는 함수가 있다고 하자. 그러나 사용자는 두 타입의 모든 속성을 모두 포함하는 하위 타입을 전달해서는 안된다.
매개변수에는 유니언 타입 VariantA | VariantB를 활용할 수 있다. 단, 타입스크립트의 타입 호환성은 구조적 서브 타이핑을 기반으로 하기 때문에 객체 리터럴을 전달하지 않는 한 파라미터의 타입보다 더 많은 속성을 가진 객체를 함수에 전달하는 것은 허용된다.
type VariantA = {
a: string,
}
type VariantB = {
b: number,
}
declare function fn(arg: VariantA | VariantB): void
const input = {a: 'foo', b: 123 }
fn(input) // 타입스크립트 컴파일러는 아무런 문제도 지적하지 않지만, 우리의 목적에는 맞지 않는다.
위 예제는 타입스크립트가 타입 오류를 발생시키지 않는다.
never를 사용하면 구조적 타이핑을 비활성화하고 사용자가 두 속성을 모두 포함하는 객체를 전달하지 못하도록 할 수 있다.
type VariantA = {
a: string
b?: never
}
type VariantB = {
b: number
a?: never
}
declare function fn(arg: VariantA | VariantB): void
const input = {a: 'foo', b: 123 }
fn(input) // ❌ 속성 'a'의 타입은 호환되지 않는다.
의도하지 않은 API 사용을 방지한다.
데이터를 읽고 저장하는 Cache 인스턴스를 생성한다고 해보자.
type Read = {}
type Write = {}
declare const toWrite: Write
declare class MyCache<T, R> {
put(val: T): boolean;
get(): R;
}
const cache = new MyCache<Write, Read>()
cache.put(toWrite) // ✅ 허용
이제, 이유야 어찌 됐든 get 메서드를 통해서만 데이터를 읽을 수 있는 읽기 전용 캐시를 원한다고 해보자. 이를 위해 put 메서드의 인수에 never를 전달해 어떤 값도 전달하지 않을 수 있다.
declare class ReadOnlyCache<R> extends MyCache<never, R> {} // 이제 MyCache 내부의 타입 매개변수 `T`가 `never`가 된다.
const readonlyCache = new ReadOnlyCache<Read>()
readonlyCache.put(data) // ❌ 'Data' 타입의 인자는 'never' 타입의 매개변수에 할당될 수 없다.
참고로, never 타입과 관련되지 않으면, 이는 파생 클래스의 좋은 예가 아닐 수 있다. 필자는 객체 지향 프로그래밍의 전문가가 아니므로, 각자 판단하자.
이론적으로 도달할 수 없는 분기를 표기한다.
infer를 사용해 조건부 타입 내에 추가 타입 변수를 생성할 경우 모든 infer 키워드에 대해 else 분기를 추가해야 한다.
type A = 'foo';
type B = A extends infer C ? (
C extends 'foo' ? true : false// 이 표현식 내에서 'C'는 'A'를 나타낸다.
) : never // 이 분기는 도달할 수 없지만, 생략도 할 수 없다.
▼ 왜 extends infer가 유용할까?
유니언 타입에서 멤버 필터링
도달할 수 없는 분기를 나타내는 것 외에도 never 타입은 조건부 타입에서 원하지 않는 타입을 필터링할 수 있다.
앞서 말한 바와 같이, never 타입은 유니언 타입에서 자동으로 제거된다. 즉, never 타입은 유니언 타입에서는 쓸모가 없다.
특정 기준에 따라 유니언 타입에서 멤버를 선택하는 유틸리티 타입을 작성할 때, never 타입은 쓸모가 없기 때문에 else 분기에 배치하기에 완벽한 타입이다.
속성 'name'에서 문자열 리터럴 'foo'를 가진 유니언 멤버를 추출하고 일치하지 않는 멤버를 필터링하는 유틸리티 타입 'ExtractTypeByName'이 있다고 해보자.
type Foo = {
name: 'foo'
id: number
}
type Bar = {
name: 'bar'
id: number
}
type All = Foo | Bar
type ExtractTypeByName<T, G> = T extends {name: G} ? T : never
type ExtractedType = ExtractTypeByName<All, 'foo'> // 결과 타입은 Foo
▼ 동작 자세히 보기
매핑된 타입의 키 필터링
타입스크립트에서 타입은 불변이다. 객체 타입에서 한 속성을 제거하고 싶으면 기존 타입을 변환하고 필터링해 새로운 타입을 만들어야 한다. 매핑된 타입의 키를 never로 조건부로 다시 매핑하면 해당 키가 필터링된다.
type Filter<Obj extends Object, ValueType> = {
[Key in keyof Obj
as ValueType extends Obj[Key] ? Key : never]
: Obj[Key]
}
interface Foo {
name: string;
id: number;
}
type Filtered = Filter<Foo, string>; // {name: string;}
제어 흐름 분석의 좁은 타입
함수 반환 값을 never로 설정하면 함수는 실행이 끝났을 때 제어권을 호출자에게 반환하지 않는다. 이를 활용하면 흐름 분석을 제어해서 타입을 좁힐 수 있다.
함수는 몇몇 이유로 아예 혹은 절대(never) 리턴을 하지 않는다. 예) 모든 코드 경로에 예외를 발생시키거나, 무한 루프이거나, 프로그램에서 종료(예: Node의 process.exit)되는 경우.
아래 예제는 foo에 대한 유니언 타입에서 undefined를 제거하기 위해 never 타입을 반환하는 함수를 사용한다.
function throwError(): never {
throw new Error();
}
let foo: string | undefined;
if (!foo) {
throwError();
}
foo; // string
또는 || 또는 ?? 연산자 다음에 throwError를 호출한다.
let foo: string | undefined;
const guaranteedFoo = foo ?? throwError(); // string
호환되지 않는 타입의 불가능한 교차 타입 표시
이는 never에 대한 실제 애플리케이션이라기보다는, 타입스크립트 언어의 동작/특성처럼 느껴질 수 있다. 그럼에도 불구하고 마주할 수 있는 일부 애매한 오류 메시지를 이해하는 것은 중요하다.
호환되지 않는 타입의 교차해서 never 타입을 얻을 수 있다.
type Res = number & string // never
그리고 아무 타입과 never 타입을 교차해서 never 타입을 얻을수 있다.
type Res = number & never // never
▼ 객체 타입에 대해서는 더 복잡하다.
never 타입은 어떻게 읽을까? (오류 메시지로부터)
명시적으로 never를 사용하지 않은 코드에서 never 타입과 관련된 오류 메시지를 받아본 적이 있을 수 있다. 이는 타입스크립트가 일반적으로 타입을 교차하기 때문이다. 타입 안정성을 유지하고 건전성을 보장하기 위해 암묵적으로 이 작업을 수행한다.
다음은 다형성 함수 입력에 대해 이전 글에서 사용한 예제다. (타입스크립트 플레이그라운드에서 해보자.)
type ReturnTypeByInputType = {
int: number
char: string
bool: boolean
}
function getRandom<T extends 'char' | 'int' | 'bool'>(
str: T
): ReturnTypeByInputType[T] {
if (str === 'int') {
// generate a random number
return Math.floor(Math.random() * 10) // ❌ 'number' 타입은 'never'타입에 할당할 수 없다.
} else if (str === 'char') {
// generate a random char
return String.fromCharCode(
97 + Math.floor(Math.random() * 26) // ❌ 'string' 타입은 'never'타입에 할당할 수 없다.
)
} else {
// generate a random boolean
return Boolean(Math.round(Math.random())) // ❌ 'boolean' 타입은 'never'타입에 할당할 수 없다.
}
}
위 함수는 전달하는 인수 타입에 따라 숫자, 문자열 또는 불리언을 반환한다. 인덱스 접근 ReturnTypeByInputType[T]을 사용하여 해당 반환 타입을 검색한다.
그러나 모든 반환 문에 대해 타입 오류(X 타입은 never타입에 할당할 수 없다.)가 발생한다. 이때 X는 분기에 따라 숫자, 문자열 또는 불리언이다.
❌가 표시된 곳은 타입스크립트가 우리 프로그램에서 문제가 발생할 수 있는 가능성을 좁혀주도록 하는 곳이다. 각 반환 값은 ReturnTypeByInputType[T](이는 런타임에 숫자, 문자열 또는 불리언 중 하나다.)에 할당할 수 있어야 한다.
리턴타입이 가능한 모든ReturnTypeByInputType[T]타입(number, string 그리고 boolean 교차 타입)에 할당이 가능해야 타입이 안정하다고 할 수 있다. 숫자, 문자열, 불리언의 교차 타입은 서로 호환되지 않으므로 never 타입이 되고, 이것이 오류 메시지에 never가 표시되는 이유이다.
이 문제를 해결하기 위해서는 타입 단언(또는 함수 오버로드)을 사용해야 한다.
- return Math.floor(Math.random() * 10) as ReturnTypeByInputType[T]
- return Math.floor(Math.random() * 10) as never
다음은 다른 예제이다.
function f1(obj: { a: number, b: string }, key: 'a' | 'b') {
obj[key] = 1; // 'number' 타입은 'never'타입에 할당할 수 없다.
obj[key] = 'x'; // 'string' 타입은 'never'타입에 할당할 수 없다.
}
obj[key]는 런타임에 key 값에 따라 문자열이나 숫자가 될 수 있다. 그러므로 타입스크립트는 안전을 위해 obj[key]에 쓰이는 모든 값은 string과 number 타입 모두와 호환되어야 한다는 제약 조건을 추가한다. 따라서 두 타입을 교차하게 되고, never 타입을 주게 된다.
never 타입은 어떻게 검사할까?
타입이 never인지 확인하는 것은 생각보다 어렵다.
아래 예제를 보자.
type IsNever<T> = T extends never ? true : false
type Res = IsNever<never> // never 🧐
Res는 true 일까, 또는 false 일까? 실제로 Res의 타입이 never임에도, 대답이 둘 다라는 사실이 놀라울 수 있다.
사실, 필자기 이것을 처음 접했을 땐 머리가 띵했다. Ryan Cavanaugh은 이것에 대해 아래와 같이 설명했다.
- 타입스크립트는 조건부 타입에 대해 자동적으로 유니언 타입을 할당한다.
- never은 빈 유니언 타입이다.
- 그러므로 할당이 발생하면 할당할 것이 없으므로 조건부 타입은 never로 평가된다.
여기서 유일한 해결 방안은 암묵적 할당을 막고 타입 매개변수를 튜플에 래핑하는 것이다.
type IsNever<T> = [T] extends [never] ? true : false;
type Res1 = IsNever<never> // 'true' ✅
type Res2 = IsNever<number> // 'false' ✅
실제로 이것은 타입스크립트 소스코드에서 가져온 것이다. 타입스크립트가 이것을 외부에 보여준다면 좋을 것이다.
-
- never 타입이 빈 타입이라는 것을 활용해 기능 제약 수행.
- 원하지 않는 유니언 타입의 멤버 및 객체 타입의 속성 필터링
- 제어 흐름 분석 지원
'실전 단아 개발 가이드' 카테고리의 다른 글
MySQL에서 리셋되는 시퀀스 만들어보기 (0) | 2024.03.28 |
---|---|
React 관리자 템플릿 (0) | 2024.02.29 |
https://ui-lib.com/ 테플릿 있는 곳 (0) | 2024.02.29 |
React Type Script 관련 팁 찾기 (0) | 2024.02.29 |
React TypeScript 템플릿 간단한 Routing 소스 (0) | 2024.02.29 |