[TypeScript] Advanced Types(고급 타입)

zeros0623·2020년 1월 8일
1

TypeScript

목록 보기
3/3
post-thumbnail

🙌안녕하세요🙌

요즘 타입스크립트 스터디를 진행하고있습니다.
일단 공식 도큐멘트를 한번 훑기로 했는데요, 제가 고급타입 부분을 맡게되어서
발표 준비 겸, 기록으로 남기기위해 포스팅합니다!

그럼 시작합니다!🚗💨

교차 타입(Intersection Types)

첫번째는 교차타입입니다!
다양한 타입을 하나로 결합해서 모든 기능을 갖춘 단일 타입을 얻는 방식입니다.

예를 들어, Person & Serializable & LoggablePerson,Serializable,Loggable의 모든 멤버를 가집니다.

믹스인(다른 포스팅에서 자세히 다룰 예정입니다)에 사용되는 교차타입, 그리고 우리가 알고 있던 객체지향의 방식과 다른 경우를 볼 수 있을 것입니다.

아래는 믹스인을 만드는 간단한 예제입니다.(짧게 설명하자면, 믹스인은 서로 다른 두 객체를 섞어서 두 객체의 기능을 모두 갖춘 하나의 객체를 만드는 것입니다 - 교차타입의 개념과 비슷하죠?)

유니온 타입(Union Type)

유니온 타입은 이름으로 유추해 보았을 때 오히려 교차타입의 동작을 따라야 할 것 같습니다. 하지만 유니온 타입의 동작은 교차타입의 그것과는 좀 다릅니다.
간단하게 표현할 수 있습니다. 이 글을 보시는 여러분들은 |(pipeline)으로 대치되는 or이란 키워드를 알고계시죠??
한글로 말하자면 '혹은'이라고하지요, 유니온 타입은 이 단어를 적용한 경우와 같습니다.
string | number라고하면 string 혹은 number겠죠? 이것이 하나의 타입이 되는 것입니다.

유니온타입은 모든 타입의 공통적인 멤버에만 접근할 수 있습니다.

유니온타입은 Fish 혹은 Bird 중에 하나라는 뜻으로 사용하니까 두 경우 전부 호환되기 위해선 공통된 멤버만 사용되어야 하겠죠? 만약 swim이라는 메소드를 사용했는데 타입이 Bird라면 메소드가 존재하지 않아 문제가 생길테니까요!

타입 가드와 차별된 타입 (Type Guards and Differentiating Types)

유니온 타입을 활용해서 두 타입 중 어느 타입이 들어오든 공통 된 메소드를 실행시켜야 할 경우를 커버할 수 있습니다. 그렇다면 어느 쪽 타입이 가지고 있지 않은 메소드를 실행해야할 떄는 어떻게 하면 좋을까요??

일반적으로 JavaScript에서는 메소드의 유무를 확인해서 실행할 수 있을 것입니다.
바로 이렇게요!

그러나, 타입스크립트에서는 컴파일 타임에 오류가 발생하게됩니다.
pet의 타입은 Fish | Bird이니 .swim이나 .fly메소드를 사용할 수 없기때문이죠.
그럼 어떻게하면 좋을까요? 우리는 타입 단언을 사용해서 이 문제를 해결할 수 있습니다!

사용자 정의 타입 가드(User-Defined Type Guards)

바로 위에서 우리는 타입 단언을 사용해서 어느 한 쪽 타입에만 존재하는 메소드를 사용할 수 있도록 코드를 작성하였습니다. 그런데 작동은하지만...타입 단언을 너무 많이 사용해서 코드가 이쁘지않아요😢😢

타입스크립트에서는 다행히도 타입 가드(type guard)라는 것을 지원합니다!
설명을 가져와서 읽어보자면,

일부 스코프에서 타입을 보장하는 런타임 검사를 수행하는 표현식.
타입 가드를 정의하려면 반환 타입이
타입 명제(type predicate) 인 함수를 정의하기만 하면 됩니다.

이라고 하네요. 한국말인데도 외계어처럼 느껴집니다...😂😂
그냥 저는 이렇게 이해했습니다. 타입을 보장해주는 쓸모있는 친구!
예제를 보면서 감을 잡아볼까요?

반환타입이 되게 낯설죠? 주석도 아닌데 왜 갑자기 영어 문장이....
저 낯선 친구가 위에 나온 타입 명제라는 친구입니다. xx는 xx다의 형식으로 사용해요.
타입 명제는 argName is Type의 형태로 작성하고, argName은 꼭 현재 함수에서 사용한 매개변수의 이름을 사용해야 합니다.

이제 이 타입가드를 이용하면 코드를 조금 더 깔끔하게 작성할 수 있습니다.

헷갈릴때는 타입가드의 return 값이 true이면 명제가 옳다는 것으로 인식한다라는 점을 기억하세요.
isFish(pet)true이면 petFish다! 이해에 도움이 되셨나요❓
이 조건을 타입스크립트가 이해하기때문에 위의 코드에서 pet.swimpet.fly부분에
서 컴파일 오류가 사라지게 되는겁니다.

typeof 타입 가드(typeof type guards)

typeof를 사용한 타입가드 또한 작성이 가능합니다!
위에서 작성한 코드를 기준으로 만들어봅시다!

타입가드는 작동하겠지만... 뭔가 타입마다 저렇게 만들어주어야하는게 굉장히 귀찮은 작업일 것 같네요💦💦
이런 단순한 반복작업을 피하기위해, 타입스크립트에서는 인라인 검사를 통해서 타입가드를 지원합니다!

인라인이라 함은, 함수의 내용만을 그대로 옮겨놓은 듯이 작성하는 것을 말합니다.

작성해야할 코드가 많이 줄었는데 여전히 타입가드는 정상적으로 작동할 것입니다.
typeof키워드를 활용한 타입가드는
typeof v === "typename" 혹은 typeof v !== "typename"의 두가지 형태를 지원합니다.
typename[number, string, boolean, symbol]중의 하나이어야합니다.
물론 다른 문자열도 가능합니다만, 타입가드로 인식하지는 않습니다.

instanceof 타입 가드(instanceof type guards)

class같은 경우는 typeof를 사용해도 항상 "object"라는 값 만을 얻을 수 있을 것입니다.
이때, 우리는 좌절하지않고 instanceof키워드를 사용하면 됩니다!

코드가 길지만 핵심은 instanceof를 사용하는 부분입니다.

작성방식은variableName instanceof constructorName.
1. 타입이 any가 아닌 경우 함수의 prototype 프로퍼티 타입
2. 해당 타입의 생성자 시그니처에 의해 반환된 타입의 결합

위의 두 방식을 저는 이렇게 이해했습니다.

이 방식은 제가 이해한 방식일 뿐입니다. 매우 주관적이니 참고만 하시기 바랍니다🤷🤷

Nullable types

기본적으로 타입체커는 nullundefined를 모든 항목에 할당 가능한 것으로 판단합니다.
타입스크립트에서는 --strictNullChecks라는 컴파일 옵션이 있는데, --noImplicitAny와 함께 필수로 써야할 옵션으로 꼽힙니다.
--strictNullChecks 옵션을 설정한 채로 null값이 들어갈 수 있는 타입을 지정하려면 유니온 타입을 설정해야합니다
let a: string | null; 이런식으로요!

선택적 매개변수와 프로퍼티(Optional parameters and properties)

--strictNullChecks를 선택적 매개 변수와 함께 쓰면 자동으로| undefined를 추가합니다:

선택적 프로퍼티도 동일합니다.

타입 가드와 타입 단언(Type guards and type assertions)

Nullable 타입은 유니온으로 구현되기 때문에 타입 가드를 사용하여 null을 제거해야합니다.

컴파일러가 null 또는 undefined를 제거할 수 없는 경우에는 타입 단언 연산자를 사용하여 수동으로 제거해야합니다.
연산자는 후위 !입니다. v!은 v의 null과 undefined를 제거합니다.

컴파일러가 namenull일 수 있다고 판단하는 이유는, 외부 함수에서 호출한 경우 중첩된 함수에 대한 모든 호출을 추적하는 것이 불가능하기 때문입니다(즉시실행함수-IIFE의 경우 가능).

IIFE를 적용해서 이렇게 코드를 작성할 수도 있습니다.

타입 별칭 (Type Aliases)

타입 별칭은 타입의 새로운 이름을 생성합니다.
그냥 타입에 다른 이름을 달아준다고 생각하면 될 거 같아요!

실제로 새로운 이름을 생성하는 것이아니라 새로운 이름만 만들어 주는 것입니다.

인터페이스와 마찬가지로, 타입 별칭도 제네릭이 될 수 있습니다.

타입 별칭을 스스로 참조할 수도 있습니다.

교차타입과 함께 꽤 쓸모있는 타입을 만들 수도 있습니다.

그러나 타입 별칭이 표현식에 다시 들어가는 것은 불가능합니다❌

Interfaces vs Type Aliases

인터페이스를 이미 공부하신 분들이라면, 이거 인터페이스와 비슷한데? 라는 생각을 하셨을지도 모르겠습니다!
이 비슷한 두 친구는 거의 비슷하지만 몇가지 다른 점을 가졌습니다.
일단, 아래코드를 에디터에 옮겨주세요.

이렇게 같은 방식으로 사용할 수 있지만, 마우스 커서를 AliasInterface에 올려보시면 다른 점이 보이실 겁니다. 하나는 type Alias = { num: number }가 나오고,
하나는 interface Interface가 나올 겁니다.
인터페이스는 어디서나 사용되는 새로운 이름을 만들기 때문인데요, 이런 이유로 만약에 타입별칭에서 오류가 생기면 오류메시지에는 타입별칭이 사용되지 않을 것입니다.

또한, 가장 중요한 차이점으로 꼽을 수 있는 확장성 부분에서 차이가 큰데요,
객체지향에서 자주 사용되는 확장(extends), 구현(implements)등이 안된다는 겁니다.
스스로 확장,구현을 할 수도 없고 다른타입에서도 확장, 구현이 불가능합니다

이런 점 때문에, 무조건 interface를 쓰는 것이 낫다는 의견도 있습니다.
하지만, 튜플 혹은 유니온 타입의 경우에는 aliases가 유용할 때도 있습니다.
상황에 맞게 사용해주시면 되겠습니다!

문자열 리터럴 타입(String Literal Types)

문자열 리터럴 타입은, "문자열을 이용한 타입" 정도로 설명할 수 있습니다.

바로 위에서 나온 yesOrNo의 예제랑 똑같이 생겼죠?

메소드 오버로드를 구현하기 위해 사용할 수도 있습니다.

숫자 리터럴 타입(Number Literal Types)

문자열 리터럴과 같습니다. 더 설명할 부분이 없네요🤷

열거형 멤버 타입(Enum Member Types)

열거형 멤버는 열거형이 선언되는 순간 하나의 타입이 됩니다.
스크린샷 2020-01-08 15.24.43.png

식별 유니온(Dicriminated Unions)

싱글톤 타입, 유니온 타입, 타입 가드 및 타입 별칭을 결합하여 식별 유니온이라는 패턴을 만들 수 있습니다.
구성 요소는 세가지입니다.
1. 공통적이고, 싱글톤 타입 프로퍼티를 가지는 타입 - discriminant
2. 그 타입들의 유니온 타입 - the union
3. 공통적인 프로퍼티를 사용한 타입가드

첫번째 구성요소는 위의 kind 프로퍼티 처럼 공통적이고 각각 식별자로 이용할 수 있는 타입을 말합니다.

두번째 구성요소는 그 타입들을 유니온해서 만든 하나의 타입입니다.

세번째 구성요소는 위의 두가지 요소를 이용하는 타입가드 입니다.

식별유니온은 하나의 타입이라기 보다는, 타입가드를 활용하는 하나의 디자인 패턴이라고 할 수 있겠습니다

엄격한 검사(Exhaustiveness checking)

위에서 타입가드를 사용해서 검사한 코드를 다시 한번 볼까요?

자 여기서 새로운 타입이 추가되었다고 가정해보겠습니다🤔🤔

그렇다면 우리는 area 함수도 바꿔주어야 할 것입니다.

두가지 방법이 있습니다.
1. --strictNullChecks 컴파일 옵션을 설정하고, area함수에 반환타입을 추가하는 것.
2. 컴파일러가 철저하게 검사하게 하기위해 never 타입을 사용하는 것.

일단 첫 번째 방법부터 볼까요?

이렇게 작성하면 number 부분에서 오류가 발생할 것입니다. 함수가 number | undefined라는 문구와 함께 말이죠

이 방법은 유니온 타입가드가 더 이상 모든 케이스를 커버할 수 없다는 것을 TypeScript가 알기 때문에 검사가 가능해지는 것입니다. 그러나 strictNullChecks가 오래된 코드에서 항상 작동하는 것은 아니라는 점을 주의해야합니다!.

두 번째 방법은 never타입을 이용한 방법입니다.

이렇게 작성하면 case가 모든 경우를 커버하지 못하면, 오류가 발생하기 때문에 철저하게 검사를 할 수 있습니다.

다형성의 this 타입(Polymorphic this types)

풀어서 얘기하자면, 다양한 모습을 가지는 this 타입 정도가 되겠네요!
이미 JavaScript에서 this 키워드에 대해서 알고 오셨을거라 생각합니다.

JavaScript에서 보통 현재 컨텍스트(문맥 - 코드 흐름에서의 위치) 정도로 알고있을텐데요,
class 에서 this는 여타 객체지향언어들의 this와 같은 개념을 동작합니다.
생성자를 통해 만들어진 현재 인스턴스를 가리킵니다.

에디터에서 v1.multiply(5) 부분에 마우스를 올려보면 다음과 같습니다.
image.png
.multiply() 메소드는 this를 리턴하는데 리턴 타입이 BasicCaculator이네요?
v2도 한번 볼까요?
image.png
리턴 타입이 ScientificCaclulator입니다. 이렇게 this가 달라지는 것을 볼 수 있습니다.

인덱스 타입(Index types)

인덱스 타입을 사용하면 동적 프로퍼티 이름을 사용하는 코드를 컴파일러가 검사하도록 할 수 있습니다.

컴파일러는 name이 실제로 Person의 프로퍼티인지 확인합니다. 이 코드에는 두 가지 새로운 연산자가 등장합니다.
1. 인덱스 타입 쿼리 연산자 - keyof T
2. 인덱스 접근 연산자 - T[K]

첫 번째, 인덱스 타입 쿼리 연산자는 타입 T에 대해 알려진 프로퍼티 이름들의 유니온 타입입니다.
예제를 보겠습니다.

일 때, 에디터에서 props에 마우스를 올려보면 뭐가 나올지 예상이 되십니까?
위의 설명대로라면 아마 "name" | "age" | "sex"가 될 것입니다.
image.png
예상한대로 동작하는 것을 볼 수 있습니다.

두 번째는 인덱스 접근 연산자 T[K]입니다.
위에 나온 예제를 보시면 pluck의 리턴 타입은 T[K]의 형태로 되어있는 걸 알 수 있습니다.

그렇다면 pluck<T, K extends keyof T>(o: T, name: K[]): T[K][]인 제네릭에서 pluck(person, ['name'])로 함수를 호출하면 어떤 결과가 될까요?
T는 person의 타입인 Person, K[]"name"[]을 가지게 됩니다.
더 나아가면 T[K]Person["name"]이 되겠네요, 위 예제에서 Person["name"]string타입이죠? 그러면 결과적으로 pluck의 리턴타입은 string[]이 되는 것입니다.

이렇게 키 값에 따라 string[]
image.png
혹은 number[]의 리턴타입을 가지게 됩니다.
image.png

잠깐✋
이렇게 사용할 때에는 타입 변수 K extends keyof T를 꼭 선언해주어야 합니다.
이 점에 유의해주세요❗️

인덱스 타입과 문자열 인덱스 시그니처(Index types and string index ignatures)

문자열 인덱스 시그니처를 기억하고 계시나요
인덱서(인덱스를 나누는 키?)를 문자열로 고정하는 지정된 형태입니다.

위 코드는 키 값은 string타입이고 값의 타입은 T라는 의미인데요,
그럼 keyof Map<type>은 무조건 string타입을 가지게 되겠죠? 키의 타입은 무조건 string이니까요

Mapped types

타입을 만들다보면 프로퍼티들이 선택적 프로퍼티이거나 혹은 readonly일 때가 있습니다.
모든 프로퍼티의 타입을 선택적으로 하거나 readonly로 만들 수 있습니다.
한가지로 전부 매핑하는 것입니다.

잠깐✋ 이 예제의 ReadonlyPartial은 이미 TypeScript에 내장되어있기때문에 그대로 사용하시면 name conflict가 일어납니다
Readonly1등으로 바꾸어 사용해주세요❗

person은 위에서 썼던 예제인 Person을 그대로 가져다 쓴 것입니다.

간단한 mapped type으로 동작 원리를 한번 볼까요❓

K는 Keys의 타입을 차례로 읽어옵니다. ex) option1, option2
for .. in구문의 동작과 유사하다고 생각하시면 될 것 같습니다.

매핑이 되면 아래 코드와 같게 됩니다.

다음은 mapped types의 가장 일반적인 템플릿이며 대부분의 경우에 적합합니다

그리고 이 mapped type은 mapped 되기 전 원본의 프로퍼티 지정자(readonly, optional)등을 모두 복사합니다.

Inference from mapped types

위에서 프로퍼티를 매핑하는 법을 알았으니, 이제 언래핑도 해봐야겠죠❓

이렇게 작성했을 때, 우리는 리턴 타입이 Person이길 기대할겁니다.
그리고 인자의 타입이 Readonly<T>이니, 우리는 Partial<T>를 매개변수로 호출할 때 에러가 나길 기대하겠지만 에러없이 실행이 됩니다.

이 부분은 후에 문제가 될 소지가 있겠죠? 예측과 다르게 동작할 수 있다는 얘기입니다.
그럼 어떻게하면 좋을까요? 정답은 리턴타입을 변수에 명시해주는 것입니다.

image.png
매개변수의 타입이 달랐기 때문에 리턴 타입 또한 보장받지 못하여 Person타입이 아니게 됩니다. 그래서 타입이 다르다는 오류를 출력하게 되죠.

타입을 명시하지않아도 타입스크립트는 동작할 수 있지만, 독이 될 수 있으므로 항상 타입을 명시해주는 습관을 들이는게 타입스크립트를 잘 사용하는 길입니다👍

조건부 타입(Conditional Types)

조건부 타입은 현재(2020.01.08) 핸드북 번역본에 들어가 있지 않은 단락입니다.
원문에서 발견하여 추가로 작성합니다.
TypeScript 2.8 버전에서 소개되었습니다.

번역본은 아니고, 원문의 내용과 검색으로 알게 된 내용을 믹스해서 작성하였습니다.

조건부 타입은 조건부 표현식에 따라 정해지는 타입니다.
여러분은 아래와 같은 표현식을 보신 적이 있으시겠죠?

조건부 타입은 위 표현식을 활용한 타입입니다.
조건부 타입은 하나의 타입이라기보다, 조건을 만족하는 타입을 구하는 표현식이라고 보는게 옳을 것 같습니다

위와 같은 형식으로 조건에 따른 타입을 지정할 수 있습니다.
위는 TU에 할당 가능하면 X, 아니면 Y 타입을 리턴한다는 의미입니다.

Type aliases와 제네릭을 이용한 간단한 예제입니다.

조건부 타입 T extends U ? X : YX | Y타입으로 해석되거나 혹은 해석이 유예됩니다(타입에 의존적이기 때문입니다.)

T 혹은 U가 타입변수를 포함하면, X | Y로의 타입해석 혹은 타입 해석 유예는 타입시스템이 "TU에 할당가능한가"에 대한 충분한 정보를 가졌는지 아닌지에 따라 정해집니다.

예시를 보면서 비교해보시죠.

위 경우는 이미 리턴 타입이 string | boolean으로 해석되는 경우입니다.

다음은 타입 해석이 연기되는 경우입니다.

분배 조건부 타입(Distributive conditional types)

검사가 이루어진 이후의 날 것의 타입인 조건부 타입을 분배 조건부 타입이라고 합니다.
분배 조건부 타입은 인스턴스화하는 동안 유니온 타입으로 분배됩니다.
예를 들어, T extends U ? X : Y이고 T의 타입 매개변수가 A | B | C라면,
이렇게 해석됩니다.
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

다음은 분배 조건부 타입이 해석되는 과정을 담은 예제입니다.

조건부 타입 T extends U ? X : Y의 인스턴스화 과정에서, 조건부 타입 내부의 T에 대한 참조는 유니온 타입의 각각의 요소로 해석됩니다. 그리고 X절(조건이 참인 경우) 에서 TU에 할당가능한 것으로 계산됩니다.

그러던중 재밌는 것을 발견했는데 type [][number] === type never라는 사실입니다,

[]number로 인덱싱하면 아무값도 얻을 수 없기 때문에 never인 것으로 이해했습니다.
결과로 보아 타입으로 해석할때 []['number']은 빈배열([])을 [number]로 인덱싱 하였을 때 얻을 수 있는 타입이 되는 것이라고 유추하였고,
['1', '2']number로 인덱싱한 결과가 리턴타입이 '1' | '2'이 되어서 검증되었다고 결론지었습니다.

매핑 타입과 함께 쓰는 조건부 타입(Conditional types with mapping types)

위에서 배운 매핑타입을 조건부 타입과 함께 사용해보겠습니다.
NonNullable이라는 타입을 분배 조건부 타입에서 예제를 통해 보셨었죠?
모든 프로퍼티를 매핑해주는 매핑타입을 조건부 타입과 함께 이용하면, 조건을 만족하는 프로퍼티들로 추려낼 수 있습니다.👍

이 또한 어떠한 과정을 통해 적용되는지 함께 보실까요❓

또한, 유니온 타입이나 교차 타입과 같이 재귀적으로 본인을 참조하는 것이 허용되지 않습니다.

조건부 타입의 타입 추론(Type inference in conditional types)

조건부 타입에서는 extends절에서 infer키워드를 사용 가능합니다.
infer키워드는 타입변수를 참조하기 위해서 사용됩니다.
같은 타입변수에 다수의 infer 키워드를 사용할 수도 있습니다.
직관적인 구조이기 때문에 이해가 어렵지 않습니다.

기본적인 형태입니다.

다음은 infer 키워드 여러개를 이용하는 예제입니다.

다음 예제는 공변적 포지션에 있는 같은 타입 변수의 여러 후보(infer로 참조 된)이 어떻게 유니온 타입이 되는지 보여줍니다.

다음은, 반변적 포지션에 있는 같은 타입 변수의 여러 후보(infer로 참조 된)이 교차 타입이 되는 과정을 보여줍니다.

공변성 & 반변성의 관한 포스팅은 이곳을 참조하였습니다.

공변적이라 함은 A <: B(A가 B의 SubType)일 때, 함수식 (T -> A) <: (T -> B)같이
좌항과 우항의 위치가 유지되는 것을 의미한다.

반변적이라 함은 A <: B(A가 B의 SubType)일 때, 함수식 (B -> T) <: (A -> T)같이
좌항과 우항의 위치가 바뀌는 것을 의미한다.

함수타입에서 반환타입은 공변적(co-variant)이고, 인자타입은 반변적(contra-variant)이다.

여러 여러 콜 시그니처(예를 들면 오버로드 된 함수의 타입)를 infer와 함께 사용할 때, 마지막으로 선언된 콜 시그니처로 부터 추론됩니다. 인자 타입 베이스의 오버로드 해석은 불가능합니다.

infer 키워드를 제약조건 절(extends ...)에서 사용하는 것은 불가능합니다.

그러나, 제약조건 절(extedns ...)에서 타입변수를 제거하고, 특정 조건부 타입으로 대체하는 것으로 거의 같은 효과를 낼 수 있습니다.

사전정의 된 조건부 타입(Predefined condtiional types)

Exclude<T, U>

T로부터 U에 할당 가능한 타입들을 제외한다.

Extract<T, U>

T로부터 U에 할당 가능한 타입들만 추출한다.

NonNullable

T로부터 null | undefined인 타입들을 제거한다.

ReturunType

T라는 함수타입의 리턴 타입을 얻는다.

InstanceType

T라는 생성자 함수의 인스턴스 타입을 얻는다.(== 어느 클래스인지)

이렇게 타입스크립트의 고급 타입 파트에 대해서 알아보았습니다!
포스팅이 이렇게 길어질 줄은 몰랐네요😢😢

다음 포스팅에서는 타입스크립트에서의 데코레이터에 대해서 알아보겠습니다
그럼, 다음포스팅에서 만나요 여러분👋👋

profile
주니어 개발자입니다. 풀스택 유니콘이 되고싶어요. 2020.02 ~ 루센트블록 재직

0개의 댓글