본문으로 건너뛰기

조합 형과 타입 가드_TypeScript 노트 9

무료2019-03-03#TypeScript#instanceof type guards#type assertions and type guards#类型保护与类型断言#TypeScript空类型检查#TypeScript nullable check

실제로는 타입 조합과 타입 구분

一.조합 형

교차 형 (intersection types)

복수 개의 타입을 조합하여 새로운 타입을 생성하며, 소스 타입 간에 "그리고" 관계가 존재합니다. 예를 들어:

interface ObjectConstructor {
  assign<T, U>(target: T, source: U): T & U;
}

(TypeScript/lib/lib.es2015.core.d.ts 에서 발췌)

Object.assignsource: U 상의 열거 가능한 프로퍼티를 target: T 에 샤로우 복사할 수 있으므로, 반환 값의 타입은 T & U 입니다

교차 형 A & BA 이기도 하고 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 | BA 또는 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 | BA 또는 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');

주의, 마지막 예는 흥미롭고, xnumber 또는 string 중 하나로, typeof 판단으로부터 number 가 아님을 알 수 있으므로, string 으로 좁혀집니다

구체적으로는, typeof 타입 가드는 2 가지 형식의 typeof 를 식별할 수 있습니다:

  • typeof v === "typename"

  • typeof v !== "typename"

그리고 typenamenumber, 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

커스텀 타입 가드

typeofinstanceof 타입 가드는 일반적인 시나리오를 만족시킬 수 있지만, 보다 특수한 경우는 커스텀 타입 가드를 통해 타입을 좁힐 수 있습니다:

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以外) 다른 모든 타입의 서브타입입니다. 따라서 nullundefined 는 다른 임의의 타입에 대입할 수 있습니다:

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 회 한 (또는 일시적) 이고, 타입 가드는 일정 스코프 하에서 유효하다는 것입니다

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성