고급 타입(Advanced Type)
Intersection Types
Intersection 타입은 여러 타입을 하나로 결합합니다. 이렇게하면 기존 타입을 모두 추가하여 필요한 모든 기능을 갖춘 단일 타입을 얻을 수 있습니다. 예를 들어, Person
& Serializable
& Loggable
은 Person
과 Serializable
이며 Loggable
입니다. 즉, 이 타입의 객체는 세 가지 타입의 모든 멤버를 갖게됩니다.
Intersection 타입의 대부분은 mixin과 고전적인 객체 지향 모습에 맞지 않는 형태에서 보게 됩니다.(JavaScript에는 이런 것들이 많이 있습니다!) 다음은 mixin을 만드는 방법을 보여주는 간단한 예제입니다.
|
|
Union Types
Union 타입은 Intersection 타입과 밀접한 관련이 있지만 매우 다르게 사용됩니다. 때로는 파라미터가 number
또는 string
이 될 것으로 기대하는 라이브러리를 실행하게 될때도 있습니다. 예를 들어, 다음과 같은 함수를 살펴보겠습니다.
|
|
padLeft
의 문제점은 padding
파라미터가 any
로 입력된다는 것입니다. 즉, number
나 string
이 아닌 파라미터를 사용하여 호출할 수 있지만 TypeScript는 해당 파라미터를 수용합니다.
|
|
전통적인 객체 지향 코드에서는 타입의 계층 구조를 만들어 두가지 타입을 추상화할 수 있습니다. 이것이 훨씬 더 명확하지만, 그것은 또한 약간 과잉대응입니다. 이러한 접근법은 이미 다른 곳에있는 함수를 사용하려는 경우에도 도움이되지 않습니다. padLeft
의 원래 버전에 대한 좋은 점 중 하나는 우리가 Primitive를 전달할 수 있다는 것이었습니다. 이는 사용법이 간단하고 간결하다는 것을 의미합니다.
any
대신에 padding
파라미터에 Union 타입을 사용할 수 있습니다.
|
|
Union 타입은 여러 타입 중 하나 일 수 있는 값을 나타냅니다. 수직 막대 (|
)를 사용하여 각 타입을 구분하므로 number | string | boolean
은 number
,string
또는boolean
일 수있는 값의 타입입니다.
만일 우리가 Union 타입을 가진 값을 가지고 있다면, Union의 모든 타입에 공통적인 멤버들만 접근할 수 있습니다.
|
|
Union 타입은 약간 까다로울 수 있지만 익숙해지기 위해서는 약간의 직감이 필요합니다. 타입이 A|B
인 값을 가지고 있다면, 우리는 A
와 B
둘 다 특정 멤버가 있음을 확실히 알 수 있습니다. 이 예제에서 Bird
에는 fly
라는 멤버가 있습니다. 그리고 Bird | Fish
타입에는 fly
메서드가 있음을 확신할 수 없습니다. 그렇기 때문에 런타임에 변수가 실제로 Fish
인 경우 pet.fly()
를 호출하면 실패할 수 있습니다.
타입 가드와 차별 타입 (Type Guards and Differentiating Types)
Union 타입은 값들이 겹쳐 질 수있는 상황을 모델링하는데 유용합니다. 우리가 Fish
를 가지고 있는지 여부를 구체적으로 알아야할 때 어떻게 해야 할까요? 두가지 값을 구별하는 JavaScript의 일반적인 방법은 멤버의 존재 여부를 확인하는 것입니다. 위에서 언급했듯이, Union 타입은 모든 구성 요소에 포함될 수 있는 멤버만 액세스할 수 있습니다.
|
|
위 코드가 작동하도록 하려면 타입 어설션을 사용해야합니다.
|
|
사용자 정의 타입 가드(User-Defined Type Guard)
타입 어설션을 여러번 사용해야 한다는 점에 주목하십시오. 일단 우리가 체크를 수행할때 각 지점 내에서 pet
의 타입을 알 수 있으면 훨씬 더 좋을 것입니다.
TypeScript에는 타입 가드(Type guard)가 있습니다. 타입 가드(Type guard)는 어떤 Scope에서 타입을 보증하는 런타임 체크를 수행하는 몇 가지 표현식입니다. 타입 가드를 정의하기 위해서, 리턴 타입이 Type predicate인 함수를 정의할 필요가 있습니다.
|
|
pet is Fish
는 위 예에서 Type predicate입니다. Predicate는 parameterName is Type
의 형식을 취합니다. 여기서 parameterName
은 현재 함수 Signature의 파라미터 이름이어야 합니다.
isFish
가 어떤 변수와 함께 호출될 때마다, 원래 타입이 호환 가능하다면 TypeScript은 그 변수를 그 특정 타입으로 추정할 것입니다.
|
|
TypeScript는 pet
이 if
문에서 Fish
라는 것을 알고 있을뿐만 아니라, else
에서는 Fish
가 아니기 때문에 Bird
가 있어야합니다.
타입 가드의 typeof
뒤로 돌아가서 Union 타입을 사용하는 padLeft
버전의 코드를 작성해 보겠습니다. 다음과 같이 Type predicates를 써서 쓸 수 있습니다.
|
|
그러나 타입이 Primitive 인지 알아내는 함수를 정의하는 것은 고통입니다. 다행스럽게도, TypeScript가 인식하기 때문에, typeof x === "number"
를 자신의 함수로 추상화할 필요가 없습니다. 즉, 이 체크를 인라인으로 작성할 수 있음을 의미합니다.
|
|
이 typeof
타입 가드는 typeof v === "typename"
과 typeof v !== "typename"
두 가지 형태로 인식됩니다. 여기서 "typename"
은 "number"
, "string"
, "boolean"
, 또는 "symbol"
이어야 합니다. TypeScript는 여러분이 다른 문자열과 비교하는 것을 못하게 하지 않지만 TypeScript는 해당 표현을 타입 가드로 인식하지 않습니다.
타입 가드의 instanceof
typeof
타입 가드를 읽었고 JavaScript에서 instanceof
연산자에 익숙하다면 아마 여기서 설명하는 내용이 익숙할 것입니다.
instanceof
타입 가드는 생성자 함수를 사용하여 타입을 좁히는 방법입니다. 예를 들어, 이전의 문자열 padding 예제를 살펴보겠습니다.
|
|
instanceof
의 오른쪽은 생성자 함수여야하며, TypeScript는 다음으로 순서로 범위를 좁힙니다.
- 타입이
any
가 아닌 경우 함수의prototype
프로퍼티 타입 - 그 타입의 생성자 Signatures 의해 리턴되는 타입의 Union 타입
Nullable types
TypeScript에는 null
과 undefined
값을 가질수 있는 두 가지 특별한 타입인 null
과 undefined
타입이 있습니다.Basic Types에서 간단히 언급했습니다. 기본적으로 타입 checker는
null
및 undefined
를 어떤것이든 할당할수 있다고 간주합니다. 그리고,null
과 undefined
는 모든 타입의 유효한 값입니다. 즉, 이 값의 할당을 막고 싶을 때조차도 any
타입에 할당되는 것을 막을 수 없다는 것을 의미합니다. null
의 고안자인 토니 호아레 (Tony Hoare)는 이것을 “billion dollar mistake” 라고 부르기도 했습니다.
--strictNullChecks
플래그는 이 문제를 해결할 수 있습니다. 변수를 선언하면 null
또는 undefined
가 자동으로 포함되지 않습니다. 하지만
Union 타입을 사용하여 명시적으로 포함 시킬수 있습니다.
|
|
TypeScript는 JavaScript 의미와 일치시키기 위해 null
과 undefined
를 다르게 취급합니다. string | null
은 string | undefined
와string | undefined | null
과 다른 타입입니다.
Optional 파라미터와 프로퍼티
--strictNullChecks
옵션은 자동으로 | undefined
를 포함 시킵니다.
|
|
Optional 프로퍼티에 대해서도 마찬가지입니다.
|
|
타입 가드와 타입 어설션 (Type guards and type assertions)
Nullable 타입은 Union으로 구현 되었기 때문에 타입 가드를 사용하여 null
을 제거해야합니다. 다행히도 JavaScript에서 작성하는 코드와 똑같습니다.
|
|
위 코드에서 null
제거 코드는 명확하지만 더 간단한 연산자를 사용할 수 있습니다.
|
|
컴파일러가 null
또는 undefined
를 제거할 수없는 경우, 타입 선언 연산자를 사용하여 수동으로 제거할 수 있습니다. 구문은 변수
뒤에 !
를 붙이는 것입니다. identifier!
는 식별자의 타입에서 null
과 undefined
를 제거합니다.
|
|
컴파일러가 중첩 함수 내에서 null
을 제거할 수 없으므로 (즉시 함수 호출 표현식 제외) 이 예제에서는 중첩 함수를 사용합니다. 중첩된 함수에 대한 모든 호출을 추적할 수 없기 때문입니다. 특히 외부 함수에서 반환하는 경우가 그렇습니다. 함수가 호출되는 위치를 알지 못하면 본문이 실행될 때 name
의 타입이 무엇인지 알 수 없습니다.
Type Aliases
타입 Alias는 타입의 새이름을 작성합니다. 타입 Alias는 때때로 인터페이스와 비슷하지만, Primitive, Union, Tuple, 그리고 여러분이 직접 작성한 타입에 이름을 붙일 수 있습니다.
|
|
타입 Alias는 실제로 새 타입을 작성하지 않으며 해당 타입을 참조하는 새 이름
을 작성합니다. Primitive의 Alias는 사용될 수 있지만 딱히 유용성은 없습니다.
인터페이스와 마찬가지로 타입 Alias도 Generic을 사용할 수 있습니다. 타입 파라미터를 추가하고 Alias 선언의 오른쪽에 사용할 수 있습니다.
|
|
또한 프로퍼티에서 타입 Alias를 참조할 수 있습니다.
|
|
Intersection 타입과 함께 우리는 Mind-bending 타입도 만들 수 있습니다.
|
|
그러나 타입 Alias가 선언의 오른쪽에 있는 곳은 사용할 수 없습니다.
|
|
Interfaces vs. Type Aliases
앞서 언급했듯이 타입 Alias는 인터페이스와 비슷한 일을할 수 있습니다. 그러나 약간의 차이가 있습니다.
한가지 차이점은 인터페이스는 어디에서나 사용되는 새로운 이름을 생성한다는 것입니다. 하지만 타입 Alias는 새 이름을 만들지 않습니다. 예를 들어 오류 메시지는 Alias를 사용하지 않습니다. 아래의 코드는 편집기에서 interfaced
위로 마우스를 가져 가면 Interface
를 반환한다고 나오지만 aliased
는 객체 리터럴 타입을 반환한다는 것을 보여줄 것입니다.
|
|
두번째로 중요한 차이점은 타입 Aliase를 확장하거나 구현할 수 없습니다. (다른 타입을 확장/구현할 수도 없습니다).
소프트웨어의 이상적인 특성은 확장에 열려 있기 때문에 가능한 경우 항상 타입 Alias 대신 인터페이스를 사용해야합니다.
반면에, 인터페이스로 어떤 모양을 표현할 수 없고 Union이나 Tuple 타입을 사용해야 한다면, 일반적으로 타입 Aliase를 사용할 수 있습니다.
문자열 리터럴 타입
문자열 리터럴 타입을 사용하면 문자열에 있어야하는 정확한 값을 지정할 수 있습니다. 실제로 문자열 리터럴 타입은 Union 타입, 타입 가드 및 타입 Alias와 잘 결합됩니다. 이러한 기능을 함께 사용하여 문자열에서 Enum 타입과 같이 작동할 수 있습니다.
|
|
세가지 허용되는 문자열 중 하나는 전달할 수 있지만 다른 문자열은 오류가 발생합니다.
|
|
오버로드를 구별하기 위해 동일한 방법으로 문자열 리터럴 타입을 사용할 수 있습니다.
|
|
Discriminated Union
문자열 리터럴 타입, Union 타입, 타입 가드 및 타입 Alias을 결합하여 Tagged union 또는 Algebraic 데이터 타입이라 불리는 Discriminated union이라는 고급 패턴을 빌드할 수 있습니다. Discriminated union은 함수형 프로그래밍에 유용합니다. 일부 언어는 자동으로 Discriminated union을 사용합니다. TypeScript는 현재 존재하는 JavaScript 패턴을 기반으로 합니다. 세가지 형식이 있습니다.
- 일반적인 문자열 리터럴 프로퍼티가 있는 타입 - Discriminated
- 타입의 합집합을 취하는 타입 Alias - Union
- 공통 프로퍼티의 타입 가드.
|
|
먼저 우리가 결합할 인터페이스를 선언합니다. 각 인터페이스는 다른 문자열 리터럴 타입을 가진 kind
프로퍼티을 가지고 있습니다. kind 프로퍼티는 Discriminant 또는 Tag라고 불립니다. 다른 프로퍼티는 각 인터페이스에 고유합니다. 인터페이스는 현재 서로 관련이 없습니다. 이제 그들을 결합 하겠습니다.
|
|
이제 Discriminated union을 사용합니다.
|
|
철저한 검사(Exhaustiveness checking)
컴파일러가 Discriminated union의 모든 변종을 커버하지 않을 때 우리에게 알려주고 싶습니다. 예를 들어 Shape
에 Triangle
을 추가하면area
도 업데이트 해야합니다.
|
|
두 가지 방법이 있습니다. 첫 번째는 --strictNullChecks
를 켜고 리턴 타입을 지정하는 것입니다.
|
|
switch
가 더 이상 철저하지 않기 때문에, TypeScript는 함수가 때때로 undefined
를 리턴할 수 있다는 것을 알고 있습니다. 명시적 리턴 타입number
를 가지고 있다면 리턴 타입이 실제로 number | undefined
입니다. 그러나 이 방법은 조금 미묘하며, 게다가 --strictNullChecks
가 오래된 코드에서 항상 작동하는 것은 아닙니다.
두번째 방법은 컴파일러가 철저히 검사하기 위해 사용하는 never
타입을 사용합니다.
|
|
여기서 assertNever
는 s
가 never
타입인지 검사합니다 - 다른 모든 케이스가 제거된 후에 남아있는 타입입니다. 여러분이 case
를 잊어 버리면 s
는 실제 타입을 가지게되고 타입 에러가 발생합니다. 이 방법을 사용하려면 추가 기능을 정의해야하지만 잊어 버렸을 때 훨씬 더 확실히 알수 있습니다.
this 타입의 다형성
this
타입의 다형성은 포함하는 클래스 또는 인터페이스의 subtype을 나타냅니다. 이를 F-바운드 다형성 (F-bounded polymorphism)이라고합니다. 따라서 계층적 인터페이스를 훨씬 쉽게 표현할 수 있습니다. 각 연산 후에 this
를 반환하는 간단한 계산기가 있습니다.
|
|
클래스는 this
타입을 사용하기 때문에 클래스를 확장할 수 있고 새로운 클래스는 변경없이 이전 메서드를 사용할 수 있습니다.
|
|
this
타입이 없으면 ScientificCalculator
는 BasicCalculator
를 확장하고 인터페이스를 유지할 수 없었을 것입니다. multiply
는 sin
메서드가 없는 BasicCalculator
를 리턴했을 것입니다. 그러나, this
타입을 사용하면 multiply
는 this
를 반환하는데, 이것은 ScientificCalculator
입니다.
인덱스 타입
인덱스 타입을 사용하면 컴파일러에서 동적 프로퍼티 이름을 사용하는 코드를 확인하도록 할 수 있습니다. 예를 들어 아래의 코드는 일반적인 JavaScript 패턴에서 객체 프로퍼티의 하위 집합을 선택하는 것입니다.
|
|
다음은 인덱스 타입 쿼리 및 인덱싱된 액세스 연산자를 사용하여 TypeScript에서 이 함수를 작성하고 사용하는 방법입니다.
|
|
컴파일러는 실제로 그 이름이 Person
의 프로퍼티인지 확인합니다. 이 예제는 몇 가지 새로운 타입 연산자를 도입합니다. 첫 번째는 인덱스 타입 쿼리 연산자인 keyof T
입니다. 어떤 타입의 T
에 대해서, keyof T
는 T
의 알려진 공개 프로퍼티 이름들의 합집합입니다.
|
|
keyof Person
은 'name' | 'age'
와 완벽하게 호환됩니다. 차이점은 Person
에 또 다른 프로퍼티 address : string
를 추가하면 keyof Person
이 자동으로 'name' | 'age' | 'address'
로 업데이트 된다는 것입니다. 그리고 pluck
과 같은 generic 문장에서 keyof
를 사용할 수 있습니다. 여기서 pluck
는 그 이전에 프로퍼티 이름을 알 수 없습니다. 즉, 컴파일러는 올바른 프라퍼티 집합을 pluck
에 전달했는지 확인합니다.
|
|
두 번째 연산자는 인덱싱된 액세스 연산자인 T[K]
입니다. 여기에서 type syntax는 expression syntax를 반영합니다. 즉, person['name']
은 Person['name']
타입을 가지고 있습니다. 이 예제에서는 단지 문자열입니다. 그리고 인덱스 타입의 질의와 마찬가지로 T[K]
를 generic 문장에서 사용할 수 있습니다. 이 문장이 실제로 힘이 생기는 곳입니다. 타입 변수 K extends keyof T
를 확실히 해야합니다. 다음은 getProperty
라는 함수를 가진 또 다른 예제입니다.
|
|
getProperty
에서 o:T
그리고 name:K
은 o[name]:T[K]
를 의미합니다. T[K]
결과를 반환하면 컴파일러는 실제 키 타입을 인스턴스화 할 것이므로 getProperty
의 리턴 타입은 요청한 프로퍼티에 따라 달라집니다.
|
|
인덱스 타입 및 문자열 인덱스 시그니처
keyof
와 T[K]
는 문자열 인덱스 시그니처와 상호 작용합니다. 문자열 인덱스 시그니처를 가진 타입을 가지고 있다면, keyof T
는 단지 문자열일 것입니다. 그리고 T[string]
은 단지 인덱스 시그니처 타입입니다.
|
|
Mapped type
일반적인 작업은 기존 타입을 가져 와서 각 프로퍼티를 선택적으로 만드는 것입니다.
|
|
또는 읽기 전용 버전을 원할 수도 있습니다.
|
|
이것은 JavaScript에서 종종 자주 발생합니다. TypeScript는 이전 타입의 Mapped type을 기반으로 새로운 타입을 생성할 수 있는 방법을 제공합니다. Mapped type에서 새 타입은 이전 타입의 각 특성을 동일한 방식으로 변환합니다. 예를 들어 readonly
또는 optional
타입의 모든 프로퍼티를 만들 수 있습니다. 다음은 몇 가지 예입니다.
|
|
그리고 사용하려면
|
|
가장 단순한 Mapped type과 그 부분을 살펴 보겠습니다.
|
|
구문은 내부에 for..in
이 있는 인덱스 시그니처의 구문과 유사합니다. 세 부분으로 나뉩니다.
- 타입 변수
K
는 차례대로 각 프로퍼티에 바인딩됩니다. - 반복 처리할 프로퍼티의 이름이 들어있는 문자열 리터럴 Union
Keys
입니다. - 프로퍼티의 결과 타입
이 간단한 예제에서 Keys
는 하드코딩된 프로퍼티 이름 목록이고 프로퍼티 타입은 항상 boolean
이므로 이 Mapped type은 다음과 같습니다.
|
|
그러나 실제 응용 프로그램은 위의 Readonly
또는 Partial
처럼 보입니다. 그들은 기존의 타입을 기반으로하며, 어떤 방식으로든 필드를 변형합니다. 그것은 keyof
와 indexed access type이 들어있는 곳입니다.
|
|
그러나 일반적인 버전을 사용하는 것이 더 유용할 수도 있습니다.
|
|
이 예제들에서, 프로퍼티 리스트는 keyof T
이고 결과 타입은 T[P]
의 변형입니다. 이것은 Mapped type의 일반적인 사용을 위한 좋은 템플릿입니다. 왜냐하면 이러한 종류의 변환은 Homomorphic이기 때문에 매핑은 T
의 프로퍼티에만 적용되고 다른 프로퍼티는 적용되지 않습니다. 컴파일러는 새로운 프로퍼티를 추가하기 전에 모든 기존 프로퍼티 modifier를 복사할 수 있음을 알고 있습니다. 예를 들어, Person.name
이 읽기 전용이면, Partial<Person>.name
은 읽기 전용이고 선택적입니다.
다음은 T [P]
가 Proxy <T>
클래스에 싸여있는 또 하나의 예입니다.
|
|
Readonly <T>
와 Partial <T>
는 매우 유용하며, Pick
와 Record
와 함께 TypeScript의 표준 라이브러리에 포함되어 있습니다.
|
|
Readonly
, Partial
과 Pick
은 Homomorphic이고 Record
는 그렇지 않습니다. Record
가 Homomorphic이 아닌 이유는 프로퍼티를 복사하는 입력 타입을 취하지 않는다는 것입니다.
|
|
Non-homomorphic 타입은 본질적으로 새로운 속성을 생성하므로 아무 곳에서나 프로퍼티 modifier를 복사할 수 없습니다.
Mapped type의 추론
이제 타입의 프로퍼티를 Wrapping하는 방법을 알았으므로 다음으로해야 할 일은 Unwrapping하는 것입니다. 다행히도, 그것은 꽤 쉽습니다.
|
|
이 Unwrapping 추론은 Homomorphic Mapped type에서만 작동합니다. Wrapping된 타입이 Homomorphic이 아닌 경우에는 Unwrapping 함수에 명시적 타입 파라미터를 지정해야합니다.
이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://www.typescriptlang.org/docs/handbook/advanced-types.html]
참고
- TypeScript 핸드북 1 - 기본 타입
- TypeScript 핸드북 2 - 변수 선언
- TypeScript 핸드북 3 - 인터페이스
- TypeScript 핸드북 4 - 클래스
- TypeScript 핸드북 5 - 함수
- TypeScript 핸드북 6 - Generic
- TypeScript 핸드북 7 - Enum
- TypeScript 핸드북 8 - 타입 유추
- TypeScript 핸드북 9 - 타입 호환성
- TypeScript 핸드북 10 - 고급 타입
- TypeScript 핸드북 11 - Symbol
- TypeScript 핸드북 12 - Iterator와 Generator