一.조합 형
교차 형 (intersection types)
복수 개의 타입을 조합하여 새로운 타입을 생성하며, 소스 타입 간에 "그리고" 관계가 존재합니다. 예를 들어:
interface ObjectConstructor {
assign<T, U>(target: T, source: U): T & U;
}
(TypeScript/lib/lib.es2015.core.d.ts 에서 발췌)
Object.assign 은 source: U 상의 열거 가능한 프로퍼티를 target: T 에 샤로우 복사할 수 있으므로, 반환 값의 타입은 T & U 입니다
교차 형 A & B 는 A 이기도 하고 B 이기도 하므로, 각 소스 타입의 모든 멤버를 가집니다:
interface A {
a: string;
}
interface B {
b: number
}
let x: A & B;
// 모두 합법
x.a;
x.b;
P.S. 이름은 intersection(교차) 이지만, 실제로는 "합집합을 구하는" 것입니다
연합 형 (union types)
교차 형과 유사하게, 연합 형은 "또는" 관계를 가진 복수 개의 타입으로부터 조합됩니다. 예를 들어:
interface DateConstructor {
new (value: number | string | Date): Date;
}
(TypeScript/lib/lib.es2015.core.d.ts 에서 발췌)
Date 생성자는 number 또는 string 또는 Date 타입의 파라미터를 받아들이며, 대응하는 타입은 number | string | Date 입니다
연합 형 A | B 는 A 또는 B 중 하나이므로, 모든 소스 타입의 공통 멤버 ("교차") 만 액세스할 수 있습니다:
interface A {
id: 'a';
a: string;
}
interface B {
id: 'b';
b: number
}
let x: A | B;
x.id;
// 에러 Property 'a' does not exist on type 'A | B'.
x.a;
// 에러 Property 'b' does not exist on type 'A | B'.
x.b;
二.타입 가드
연합 형은타입으로 구성된 열거 형에 상당하므로, 그 구체 타입을 확정할 수 없습니다:
연합 형
A | B는A또는B중 하나
이는 함수 시그니처에서는 문제가 없지만, 함수 구현에서는, 통상은 구체 타입을 구분할 필요가 있습니다. 예를 들어:
let createDate: (value: number | string | Date) => Date;
createDate = function(value) {
let date: Date;
if (typeof value === 'string') {
value = value.replace(/-/g, '/');
// ...
}
else if (typeof value === 'number') {/*...*/}
else if (value instanceof Date) {/*...*/}
return date;
};
따라서, 이러한 시나리오에서는, "넓은" 연합 형을 "좁게" 하여 구체 타입으로 할 필요가 있습니다. 타입의 관점에서, 위의 코드는 이상적으로는 다음과 같습니다:
function(value) {
// 여기서는, value 는 연합 형으로, number 또는 string 또는 Date 중 하나
if (typeof value === 'string') {
// 이 분기에서는, value 는 string
}
else if (typeof value === 'number') {
// 이 분기에서는, value 는 number
}
else if (value instanceof Date) {
// 이 분기에서는, value 는 Date
}
// 여기서는, value 는 연합 형으로, number 또는 string 또는 Date 중 하나
}
즉, 타입 시스템에 "들어봐, 지금 이 것의 구체 타입을 알았으니, 작게 좁혀줘"라고伝える 메커니즘이 필요합니다
그리고, 이 메커니즘이*타입 가드 (type guard)*입니다
A type guard is some expression that performs a runtime check that guarantees the type in some scope.
typeof 타입 가드
typeof variable === 'type' 는 기본 타입을 확정하는 관용적인 수법이므로, TypeScript 는 typeof 를 식별하고, 대응하는 분기 하의 연합 형을 자동으로 좁힙니다:
let x: number | string;
if (typeof x === 'string') {
// 올바른 typeof 타입 가드, 자동으로 string 으로 좁혀짐
x.toUpperCase();
}
switch 문, && 등의 다른 분기 구조에서도 동일하게 적용됩니다:
switch (typeof x) {
case 'number':
// 올바른 typeof 타입 가드
x.toFixed();
break;
}
// 올바른 typeof 타입 가드
typeof x !== 'number' && x.startsWith('xxx');
주의, 마지막 예는 흥미롭고, x 는 number 또는 string 중 하나로, typeof 판단으로부터 number 가 아님을 알 수 있으므로, string 으로 좁혀집니다
구체적으로는, typeof 타입 가드는 2 가지 형식의 typeof 를 식별할 수 있습니다:
-
typeof v === "typename" -
typeof v !== "typename"
그리고 typename 은 number, string, boolean 또는 symbol 만으로, 나머지의 typeof 검출 결과는 그다지 신뢰할 수 없으므로 (상세는 typeof 참조), 타입 가드로서는 사용되지 않습니다. 예를 들어:
let x: any;
if (typeof x === 'function') {
// any 타입, typeof 타입 가드는 적용되지 않음
x;
}
if (typeof x === 'object') {
// any 타입, typeof 타입 가드는 적용되지 않음
x;
}
P.S. 관련 토론은, typeof a === "object" does not type the object as Object 참조
instanceof 타입 가드
typeof 가 기본 타입을 검출하는 것과 마찬가지로, instanceof 는 인스턴스와 "클래스"의 소속 관계를 검출하므로, 이 것도 타입 가드입니다. 예를 들어:
let x: Date | RegExp;
if (x instanceof RegExp) {
// 올바른 instanceof 타입 가드, 자동으로 RegExp 인스턴스 타입으로 좁혀짐
x.test('');
}
else {
// 올바른 자동으로 Date 인스턴스 타입으로 좁혀짐
x.getTime();
}
구체적으로는, instanceof 우측은 생성자일 필요가 있으며, 이때 좌측의 타입은 이하로 좁혀집니다:
-
그 클래스 인스턴스의 타입 (생성자
prototype속성의 타입) -
(생성자에 오버로드 버전이 존재하는 경우) 생성자의 반환 타입으로 구성된 연합 형
예를 들어:
// Case1 그 클래스 인스턴스의 타입
let x;
if (x instanceof Date) {
// x 는 any 에서 Date 로 좁혀짐
x.getTime();
}
// Case2 생성자의 반환 타입으로 구성된 연합 형
interface DateOrRegExp {
new(): Date;
new(value?: string): RegExp;
}
let A: DateOrRegExp;
let y;
if (y instanceof A) {
// y 는 any 에서 RegExp | Date 로 좁혀짐
y;
}
P.S. instanceof 타입 가드의 더 많은 정보는, 4.24 Type Guards 참조
P.S. 또한, [class 는 이중의 타입 의미를 가진다](/articles/类-typescript 노트 4/#articleHeader8), TypeScript 코드 내에서의 표현 형식은 다음과 같습니다:
-
클래스의 타입:
typeof className -
클래스 인스턴스의 타입:
typeof className.prototype또는className
예를 들어:
class A {
static prop = 'prop';
id: 'b'
}
// 클래스의 타입
let x: typeof A;
x.prop;
// 에러 id 는 인스턴스 속성, 클래스 상에는 존재하지 않음
x.id;
// 클래스 인스턴스의 타입
let y: typeof A.prototype;
let z: A;
// 양자의 타입은 동등
z = y;
// 에러 prop 는 정적 속성, 인스턴스 상에는 존재하지 않음
z.prop;
z.id;
즉, 클래스 인스턴스의 타입은 생성자 prototype 속성의 타입과 동등합니다. 그러나 이는TypeScript 의 컴파일 시에만 성립하며, JavaScript 런타임 개념과 충돌합니다:
class A {}
class B extends A {}
// 생성자 prototype 속성은 부모 클래스 인스턴스, 그 타입은 부모 클래스 인스턴스의 타입
B.prototype instanceof A === true
커스텀 타입 가드
typeof 와 instanceof 타입 가드는 일반적인 시나리오를 만족시킬 수 있지만, 보다 특수한 경우는 커스텀 타입 가드를 통해 타입을 좁힐 수 있습니다:
interface RequestOptions {
url: string;
onSuccess?: () => void;
onFailure?: () => void;
}
// 커스텀 타입 가드, 파라미터 타입 any 를 RequestOptions 로 좁힘
function isValidRequestOptions(opts: any): opts is RequestOptions {
return opts && opts.url;
}
let opts;
if (isValidRequestOptions(opts)) {
// opts 는 any 에서 RequestOptions 로 좁혀짐
opts.url;
}
커스텀 타입 가드는 일반적인 함수 선언과 유사하지만, 반환 타입 부분은*타입 술어 (type predicate)*입니다:
parameterName is Type
그 중에서 parameterName 은 현재의 함수 시그니처 중의 파라미터명일 필요가 있습니다. 예를 들어 위의 opts is RequestOptions
타입 술어를 가진 함수를 호출한 후, 건네진 파라미터의 타입은 지정 타입으로 좁혀지며, 전 2 개의 타입 가드의 동작과 일치합니다:
let isNumber: (value: any) => value is number;
let x: string | number;
if (isNumber(x)) {
// number 로 좁혀짐
x.toFixed(2);
}
else {
// number 가 아니면 string
x.toUpperCase();
}
三.Nullable 과 연합 형
TypeScript 에는 공 타입 (Void) 이 2 개 있습니다: Undefined 와 Null 로, (Never以外) 다른 모든 타입의 서브타입입니다. 따라서 null 과 undefined 는 다른 임의의 타입에 대입할 수 있습니다:
let x: string;
x = null;
x = undefined;
// 런타임 에러, 컴파일 시에는 에러가 되지 않음
x.toUpperCase();
타입으로 보면, Nullable 타입은 원래 타입과 null | undefined 로 구성된 연합 형에 상당합니다 (위의 예에서는, let x: string | null | undefined; 에 상당)
이는 타입 체크가 그다지 신뢰할 수 없는 것을 의미하며, undefined/null.xxx 와 같은 에러를 여전히 피할 수 없기 때문입니다
strictNullChecks
공 타입의 잠재적인 문제에 대해, TypeScript 는 --strictNullChecks 옵션을 제공하며, 켜면 공 타입을 엄격하게 체크합니다:
let x: string;
// 에러 Type 'null' is not assignable to type 'string'.
x = null;
// 에러 Type 'undefined' is not assignable to type 'string'.
x = undefined;
공이 될 수 있는 타입에 대해서는, 명시적으로 선언할 필요가 있습니다:
let y: string | undefined;
y = undefined;
// Type 'null' is not assignable to type 'string | undefined'.
y = null;
동시에, 옵션 파라미터와 옵션 프로퍼티는 자동으로 | undefined 가 붙습니다. 예를 들어:
function createDate(value?: string) {
// 에러 Object is possibly 'undefined'.
value.toUpperCase();
}
interface Animal {
color: string;
name?: string;
}
let x: Animal;
// 에러 Type 'undefined' is not assignable to type 'string'.
x.color = undefined;
// 에러 Object is possibly 'undefined'.
x.name.toUpperCase();
유사한 공 값 관련 문제는 모두 노출되므로, 이 관점에서, 공 타입의 엄격 체크는 일종의컴파일 시 체크로 공 값을 추적하는 능력에 상당합니다
! 접미사 타입 어설션
Nullable 타입은 실질적으로 연합 형이므로, 동일하게 타입의 좁힘 문제에 직면합니다. 이에 대해, TypeScript 는 직관적인 타입 가드를 제공합니다:
function createDate(value: string | undefined) {
// string 으로 좁혀짐
value = value || 'today';
value.toUpperCase();
}
자동 타입 가드가 처리할 수 없는 시나리오에서는, 간단하게 ! 접미사를 통해 | undefined | null 성분을 제거할 수 있습니다:
function fixed(name: string | null): string {
function postfix(epithet: string) {
// ! 를 통해 타입 중의 null 성분을 제거하여, string 으로 좁힘
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}
identifier! 는 [타입 어설션](/articles/기본 타입-typescript 노트 2/#articleHeader4) 에 상당합니다 (타입 가드와는 다릅니다):
let x: string | undefined | null;
x!.toUpperCase();
// 에 상당
(<string>x).toUpperCase();
// 또는
(x as string).toUpperCase();
// Object is possibly 'null' or 'undefined'.
x.toUpperCase();
P.S. 타입 어설션과 타입 가드의 차이는, 어설션은 1 회 한 (또는 일시적) 이고, 타입 가드는 일정 스코프 하에서 유효하다는 것입니다
아직 댓글이 없습니다