사용자 도구

사이트 도구


typescript:advanced_type

Advanced Type

Table of contents

  • Intersection Types
  • Union Types
  • Type Guards and Differentiating Types
    • User-Defined Type Guards
      • Using type predicates
      • Using the in operator
    • typeof type guards
    • instanceof type guards
  • Nullable types
    • Optional parameters and properties
    • Type guards and type assertions
  • Type Aliases
    • Interfaces vs. Type Aliases
  • String Literal Types
  • Numeric Literal Types
  • Enum Member Types
  • Discriminated Unions
    • Exhaustiveness checking
  • Polymorphic this types
  • Index types
    • Index types and string index signatures
  • Mapped types
    • Inference from mapped types
  • Conditional Types
    • Distributive conditional types
    • Type inference in conditional types
    • Predefined conditional types

Intersection Types

Intersection 타입은 여러 타입을 하나로 결합합니다. 이렇게하면 기존 타입을 모두 추가하여 필요한 모든 기능을 갖춘 단일 타입을 얻을 수 있습니다. 예를 들어, Person & Serializable & Loggable은 Person과 Serializable이며 Loggable입니다. 즉, 이 타입의 객체는 세 가지 타입의 모든 멤버를 갖게됩니다.

Intersection 타입의 대부분은 mixin과 고전적인 객체 지향 모습에 맞지 않는 형태에서 보게 됩니다.(JavaScript에는 이런 것들이 많이 있습니다!) 다음은 mixin을 만드는 방법을 보여주는 간단한 예제입니다.

function extend<First, Second>(first: First, second: Second): First & Second {
    const result: Partial<First & Second> = {};
    for (const prop in first) {
        if (first.hasOwnProperty(prop)) {
            (result as First)[prop] = first[prop];
        }
    }
    for (const prop in second) {
        if (second.hasOwnProperty(prop)) {
            (result as Second)[prop] = second[prop];
        }
    }
    return result as First & Second;
}
 
class Person {
    constructor(public name: string) { }
}
 
interface Loggable {
    log(name: string): void;
}
 
class ConsoleLogger implements Loggable {
    log(name) {
        console.log(`Hello, I'm ${name}.`);
    }
}
 
const jim = extend(new Person('Jim'), ConsoleLogger.prototype);
jim.log(jim.name);

Union Types

Union 타입은 Intersection 타입과 밀접한 관련이 있지만 매우 다르게 사용됩니다. 때로는 파라미터가 number 또는 string이 될 것으로 기대하는 라이브러리를 실행하게 될때도 있습니다. 예를 들어, 다음과 같은 함수를 살펴보겠습니다.

/**
 * 문자열을 가져 와서 왼쪽에 'padding'을 추가합니다.
 * 'padding'이 문자열이면 'padding'이 왼쪽에 추가됩니다.
 * 'padding'이 숫자 인 경우 해당 개수의 공백이 왼쪽에 추가됩니다.
 */
function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}
padLeft("Hello world", 4); // returns "    Hello world"

padLeft의 문제점은 padding 파라미터가 any로 입력된다는 것입니다. 즉, number나 string이 아닌 파라미터를 사용하여 호출할 수 있지만 TypeScript는 해당 파라미터를 수용합니다.

let indentedString = padLeft("Hello world", true); // 컴파일 타임에는 통과 하지만 runtime에 실패가 발생합니다.

전통적인 객체 지향 코드에서는 타입의 계층 구조를 만들어 두가지 타입을 추상화할 수 있습니다. 이것이 훨씬 더 명확하지만, 그것은 또한 약간 과잉대응입니다. 이러한 접근법은 이미 다른 곳에있는 함수를 사용하려는 경우에도 도움이되지 않습니다. padLeft의 원래 버전에 대한 좋은 점 중 하나는 우리가 Primitive를 전달할 수 있다는 것이었습니다. 이는 사용법이 간단하고 간결하다는 것을 의미합니다.

any 대신에 padding 파라미터에 Union 타입을 사용할 수 있습니다.

/**
 * 문자열을 가져 와서 왼쪽에 "패딩"을 추가합니다.
 * 'padding'이 문자열이면 'padding'이 왼쪽에 추가됩니다.
 * 'padding'이 숫자 인 경우 해당 개수의 공백이 왼쪽에 추가됩니다.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}
let indentedString = padLeft("Hello world", true); // errors during compilation

Union 타입은 여러 타입 중 하나 일 수 있는 값을 나타냅니다. 수직 막대(|)를 사용하여 각 타입을 구분하므로 number | string | booleannumber, string 또는 boolean 일 수 있는 값의 타입입니다.

만일 우리가 Union 타입을 가진 값을 가지고 있다면, Union의 모든 타입에 공통적인 멤버들만 접근할 수 있습니다.

interface Bird {
    fly();
    layEggs();
}
interface Fish {
    swim();
    layEggs();
}
function getSmallPet(): Fish | Bird {
    // ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

Union 타입은 약간 까다로울 수 있지만 익숙해지기 위해서는 약간의 직감이 필요합니다. 타입이 A|B인 값을 가지고 있다면, 우리는 A와 B 둘 다 특정 멤버가 있음을 확실히 알 수 있습니다. 이 예제에서 Bird에는 fly라는 멤버가 있습니다. 그리고 Bird | Fish 타입에는 fly 메서드가 있음을 확신할 수 없습니다. 그렇기 때문에 런타임에 변수가 실제로 Fish 인 경우 pet.fly()를 호출하면 실패할 수 있습니다.

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

Union 타입은 값들이 겹쳐 질 수있는 상황을 모델링하는데 유용합니다. 우리가 Fish를 가지고 있는지 여부를 구체적으로 알아야할 때 어떻게 해야 할까요? 두가지 값을 구별하는 JavaScript의 일반적인 방법은 멤버의 존재 여부를 확인하는 것입니다. 위에서 언급했듯이, Union 타입은 모든 구성 요소에 포함될 수 있는 멤버만 액세스할 수 있습니다.

let pet = getSmallPet();
// 이러한 각 프로퍼티의 액세스는 오류를 발생시킵니다.
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

위 코드가 작동하도록 하려면 타입 어설션을 사용해야합니다.

let pet = getSmallPet();
if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

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

타입 어설션을 여러번 사용해야 한다는 점에 주목하십시오. 일단 우리가 체크를 수행할때 각 지점 내에서 pet의 타입을 알 수 있으면 훨씬 더 좋을 것입니다.

TypeScript에는 타입 가드(Type guard)가 있습니다. 타입 가드(Type guard)는 어떤 Scope에서 타입을 보증하는 런타임 체크를 수행하는 몇 가지 표현식입니다.

형식 술어 사용(Using type predicates)

타입 가드를 정의하기 위해서, 리턴 타입이 Type predicate인 함수를 정의할 필요가 있습니다.

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

pet is Fish는 위 예에서 Type predicate입니다. Predicate는 parameterName is Type의 형식을 취합니다. 여기서 parameterName은 현재 함수 Signature의 파라미터 이름이어야 합니다.

isFish가 어떤 변수와 함께 호출될 때마다, 원래 타입이 호환 가능하다면 TypeScript은 그 변수를 그 특정 타입으로 추정할 것입니다.

// 'swim'과 'fly'에 대한 호출은 이제 모두 괜찮습니다.
if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

TypeScript는 pet이 if문에서 Fish라는 것을 알고 있을뿐만 아니라, else에서는 Fish가 아니기 때문에 Bird가 있어야합니다.

Using the in operator

이제 in 연산자는 유형에 대한 축소 표현식의 역할을 합니다.

n in x 표현식에서 n은 문자열 리터럴 또는 문자열 리터럴 유형이고 x는 공용체 유형이며 “true”분기는 선택적 또는 필수 속성 n이있는 유형으로 좁혀지고 “false”분기는 다음 유형으로 좁혀집니다. 선택적 또는 누락 된 특성 n을 가짐.

function move(pet: Fish | Bird) {
    if ("swim" in pet) {
        return pet.swim();
    }
    return pet.fly();
}

타입 가드의 typeof

뒤로 돌아가서 Union 타입을 사용하는 padLeft 버전의 코드를 작성해 보겠습니다. 다음과 같이 Type predicates를 써서 쓸 수 있습니다.

function isNumber(x: any): x is number {
    return typeof x === "number";
}
function isString(x: any): x is string {
    return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

그러나 타입이 Primitive 인지 알아내는 함수를 정의하는 것은 고통입니다. 다행스럽게도, TypeScript가 인식하기 때문에, typeof x === “number”를 자신의 함수로 추상화할 필요가 없습니다. 즉, 이 체크를 인라인으로 작성할 수 있음을 의미합니다.

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

이 typeof 타입 가드는 typeof v === “typename”과 typeof v !== “typename” 두 가지 형태로 인식됩니다. 여기서 “typename”은 “number”, “string”, “boolean”, 또는 “symbol”이어야 합니다. TypeScript는 여러분이 다른 문자열과 비교하는 것을 못하게 하지 않지만 TypeScript는 해당 표현을 타입 가드로 인식하지 않습니다.

타입 가드의 instanceof

typeof 타입 가드를 읽었고 JavaScript에서 instanceof 연산자에 익숙하다면 아마 여기서 설명하는 내용이 익숙할 것입니다.

instanceof 타입 가드는 생성자 함수를 사용하여 타입을 좁히는 방법입니다. 예를 들어, 이전의 문자열 padding 예제를 살펴보겠습니다.

interface Padder {
    getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}
class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}
function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}
// 'SpaceRepeatingPadder | StringPadder' 타입입니다.
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
    padder; // 타입이 'SpaceRepeatingPadder'로 좁혀졌습니다.
}
if (padder instanceof StringPadder) {
    padder; // 타입이 'StringPadder'로 좁혀졌습니다.
}

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 타입을 사용하여 명시적으로 포함 시킬수 있습니다.

let s = "foo";
s = null; // error, 'null' is not assignable to 'string'
let sn: string | null = "bar";
sn = null; // ok
sn = undefined; // error, 'undefined' is not assignable to 'string | null'

TypeScript는 JavaScript 의미와 일치시키기 위해 nullundefined를 다르게 취급합니다. string | nullstring | undefinedstring | undefined | null과 다른 타입입니다.

Optional 파라미터와 프로퍼티

–strictNullChecks으로써, 선택 매개변수(optional parameter)는 자동적으로 | undefined를 추가합니다.

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null'은 'number | undefined' 타입에 할당할 수 없습니다.

Optional 프로퍼티에 대해서도 마찬가지입니다.

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

타입 가드와 타입 어설션 (Type guards and type assertions)

Nullable 타입은 Union으로 구현 되었기 때문에 타입 가드를 사용하여 null을 제거해야합니다. 다행히도 JavaScript에서 작성하는 코드와 똑같습니다.

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

위 코드에서 null 제거 코드는 명확하지만 더 간단한 연산자를 사용할 수 있습니다.

function f(sn: string | null): string {
    return sn || "default";
}

컴파일러가 null 또는 undefined를 제거할 수없는 경우, 타입 선언 연산자를 사용하여 수동으로 제거할 수 있습니다. 구문은 변수 뒤에 !를 붙이는 것입니다. identifier!는 식별자의 타입에서 null과 undefined를 제거합니다.

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}
function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

컴파일러가 중첩 함수 내에서 null을 제거할 수 없으므로 (즉시 함수 호출 표현식 제외) 이 예제에서는 중첩 함수를 사용합니다. 중첩된 함수에 대한 모든 호출을 추적할 수 없기 때문입니다. 특히 외부 함수에서 반환하는 경우가 그렇습니다. 함수가 호출되는 위치를 알지 못하면 본문이 실행될 때 name의 타입이 무엇인지 알 수 없습니다.

Type Aliases

타입 Alias는 타입에 대한 새이름을 작성합니다. 타입 Alias는 때때로 인터페이스와 비슷하지만, Primitive, Union, Tuple, 그리고 여러분이 직접 작성한 타입에 이름을 붙일 수 있습니다.

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === "string") {
        return n;
    }
    else {
        return n();
    }
}

타입 Alias는 실제로 새 타입을 작성하지 않으며 해당 타입을 참조하는 새 이름을 작성합니다. Primitive의 Alias는 사용될 수 있지만 딱히 유용성은 없습니다.

Interface와 마찬가지로 타입 Alias도 Generic을 사용할 수 있습니다. 타입 파라미터를 추가하고 Alias 선언의 오른쪽에 사용할 수 있습니다.

type Container<T> = { value: T };

또한 프로퍼티에서 타입 Alias를 참조할 수 있습니다.

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

Intersection 타입과 함께 우리는 Mind-bending 타입도 만들 수 있습니다.

type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
    name: string;
}
var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

그러나 타입 Alias가 그 선언의 오른쪽에 있는 곳은 사용할 수 없습니다.

type Yikes = Array<Yikes>; // error

Interfaces vs. Type Aliases

앞서 언급했듯이 타입 Alias는 인터페이스와 비슷한 일을 할 수 있습니다. 그러나 약간의 차이가 있습니다.

한가지 차이점은 인터페이스는 어디에서나 사용되는 새로운 이름을 생성한다는 것입니다. 하지만 타입 Alias는 새 이름을 만들지 않습니다. 예를 들어 오류 메시지는 Alias를 사용하지 않습니다. 아래의 코드는 편집기에서 interfaced 위로 마우스를 가져 가면 Interface 를 반환한다고 나오지만 aliased는 객체 리터럴 타입을 반환한다는 것을 보여줄 것입니다.

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

이전 버전의 TypeScript에서는 타입 Aliase을 확장하거나 구현할 수 없으며 다른 형식을 확장/구현할 수도 없었습니다. 버전 2.7부터는 새로운 intersection type을 생성하여 타입 Alias을 확장 할 수 있습니다. Cat = Animal & {purrs : true}

소프트웨어의 이상적인 특성이 확장되기 때문에 가능한 경우 타입 Alias에 대한 Interface를 사용해야합니다.

반면에, Interface로 어떤 모양을 표현할 수 없고 Union이나 Tuple 타입을 사용해야 한다면, 일반적으로 타입 Aliase를 사용할 수 있습니다.

문자열 리터럴 타입

문자열 리터럴 타입을 사용하면 문자열에 있어야하는 정확한 값을 지정할 수 있습니다. 실제로 문자열 리터럴 타입은 Union 타입, 타입 가드 및 타입 Alias와 잘 결합됩니다. 이러한 기능을 함께 사용하여 문자열에서 Enum 타입과 같이 작동할 수 있습니다.

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! null 또는 undefined를 넘겨서는 안됩니다.
        }
    }
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy"는 여기에 사용할 수 없습니다.

세가지 허용되는 문자열 중 하나는 전달할 수 있지만 다른 문자열은 오류가 발생합니다.

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

오버로드를 구별하기 위해 동일한 방법으로 문자열 리터럴 타입을 사용할 수 있습니다.

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

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

TypeScript는 또한 numeric literal types을 가진다.

function rollDics(): 1 | 2 | 3 | 4 | 5 | 6 {
  // ...
}

이들은 명시적으로 작성되는 경우는 거의 없으며, 좁히는 것이 버그를 잡을 수있을 때 유용 할 수 있습니다.

function foo(x: number) {
  if (x !== 1 || x !== 2) {
    // ~~~~~
    // Operator '!==' cannot be applied to type '1' and '2'.
  }
}

즉, x가 1이어야 2와 비교가능하다. 위의 검사가 잘못된 비교를하고 있음을 의미합니다.

Enum Member Types

Enums 섹션에서 언급했듯이 enum 멤버는 모든 멤버가 리터럴로 초기화 될 때 유형을가집니다.

많은 사람들이 “singleton types”과 “리터럴 유형”을 같은 의미로 사용하지만 “싱글 톤 유형”에 대해 이야기 할 때 많은 부분에서 숫자/문자열 리터럴 타입뿐 아니라 enum 타입을 모두 참조합니다.

식별된 유니온(Discriminated Union)

문자열 리터럴 타입, Union 타입, 타입 가드 및 타입 Alias을 결합하여 Tagged union 또는 Algebraic 데이터 타입이라 불리는 Discriminated union이라는 고급 패턴을 빌드할 수 있습니다. Discriminated union은 함수형 프로그래밍에 유용합니다. 일부 언어는 자동으로 Discriminated union을 사용합니다. TypeScript는 현재 존재하는 JavaScript 패턴을 기반으로 합니다. 세가지 형식이 있습니다.

  1. 일반적인 문자열 리터럴 프로퍼티가 있는 타입 - Discriminated
  2. 타입의 합집합을 취하는 타입 Alias - Union
  3. 공통 프로퍼티의 타입 가드.
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

먼저 우리가 결합할 인터페이스를 선언합니다. 각 인터페이스는 다른 문자열 리터럴 타입을 가진 kind 프로퍼티을 가지고 있습니다. kind 프로퍼티는 Discriminant 또는 Tag라고 불립니다. 다른 프로퍼티는 각 인터페이스에 고유합니다. 인터페이스는 현재 서로 관련이 없습니다. 이제 그들을 결합 하겠습니다.

type Shape = Square | Rectangle | Circle;

이제 Discriminated union을 사용합니다.

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

철저한 검사(Exhaustiveness checking)

컴파일러가 Discriminated union의 모든 변종을 커버하지 않을 때 우리에게 알려주고 싶습니다. 예를 들어 Shape에 Triangle을 추가하면 area도 업데이트 해야합니다.

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
}

두 가지 방법이 있습니다. 첫 번째는 –strictNullChecks를 켜고 리턴 타입을 지정하는 것입니다.

function area(s: Shape): number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

switch가 더 이상 철저하지 않기 때문에, TypeScript는 함수가 때때로 undefined를 리턴할 수 있다는 것을 알고 있습니다. 명시적 리턴 타입number를 가지고 있다면 리턴 타입이 실제로 number | undefined입니다. 그러나 이 방법은 조금 미묘하며, 게다가 –strictNullChecks가 오래된 코드에서 항상 작동하는 것은 아닙니다.

두번째 방법은 컴파일러가 철저히 검사하기 위해 사용하는 never 타입을 사용합니다.

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}

여기서 assertNever는 s가 never 타입인지 검사합니다 - 다른 모든 케이스가 제거된 후에 남아있는 타입입니다. 여러분이 case를 잊어 버리면 s는 실제 타입을 가지게되고 타입 에러가 발생합니다. 이 방법을 사용하려면 추가 기능을 정의해야하지만 잊어 버렸을 때 훨씬 더 확실히 알수 있습니다.

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

this 타입의 다형성은 포함하는 클래스 또는 인터페이스의 subtype을 나타냅니다. 이를 F-바운드 다형성 (F-bounded polymorphism)이라고합니다. 따라서 계층적 인터페이스를 훨씬 쉽게 표현할 수 있습니다. 각 연산 후에 this를 반환하는 간단한 계산기가 있습니다.

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}
let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

클래스는 this 타입을 사용하기 때문에 클래스를 확장할 수 있고 새로운 클래스는 변경없이 이전 메서드를 사용할 수 있습니다.

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}
let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

this 타입이 없으면 ScientificCalculator는 BasicCalculator를 확장하고 인터페이스를 유지할 수 없었을 것입니다. multiply는 sin 메서드가 없는 BasicCalculator를 리턴했을 것입니다. 그러나, this 타입을 사용하면 multiply는 this를 반환하는데, 이것은 ScientificCalculator입니다.

인덱스 타입(Index types)

인덱스 타입을 사용하면 컴파일러에서 동적 프로퍼티 이름을 사용하는 코드를 확인하도록 할 수 있습니다. 예를 들어 아래의 코드는 일반적인 JavaScript 패턴에서 객체 프로퍼티의 하위 집합을 선택하는 것입니다.

function pluck(o, names) {
    return names.map(n => o[n]);
}

다음은 index type queryindexed access 연산자를 사용하여 TypeScript에서 이 함수를 작성하고 사용하는 방법입니다.

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}
interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]

컴파일러는 실제로 그 이름이 Person의 프로퍼티인지 확인합니다. 이 예제는 몇 가지 새로운 타입 연산자를 도입합니다. 첫 번째는 인덱스 타입 쿼리 연산자인 keyof T입니다. 어떤 타입의 T에 대해서, keyof T는 T의 알려진 공개 프로퍼티 이름들의 합집합입니다.

let personProps: keyof Person; // 'name' | 'age'

keyof Person은 'name' | 'age'와 완벽하게 호환됩니다. 차이점은 Person에 또 다른 프로퍼티 address : string를 추가하면 keyof Person이 자동으로 'name' | 'age' | 'address'로 업데이트 된다는 것입니다. 그리고 pluck과 같은 generic 문장에서 keyof를 사용할 수 있습니다. 여기서 pluck는 그 이전에 프로퍼티 이름을 알 수 없습니다. 즉, 컴파일러는 올바른 프라퍼티 집합을 pluck에 전달했는지 확인합니다.

pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'

두 번째 연산자는 인덱싱된 액세스 연산자인 T[K]입니다. 여기에서 type syntax는 expression syntax를 반영합니다. 즉, Person['name']은 Person['name'] 타입을 가지고 있습니다. 이 예제에서는 단지 문자열입니다. 그리고 인덱스 타입의 질의와 마찬가지로 T[K]를 generic 문장에서 사용할 수 있습니다. 이 문장이 실제로 힘이 생기는 곳입니다. 타입 변수 K extends keyof T를 확실히 해야합니다. 다음은 getProperty라는 함수를 가진 또 다른 예제입니다.

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

getProperty에서 o:T 그리고 name:K은 o[name]:T[K]를 의미합니다. T[K] 결과를 반환하면 컴파일러는 실제 키 타입을 인스턴스화 할 것이므로 getProperty의 리턴 타입은 요청한 프로퍼티에 따라 달라집니다.

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

Index types and string index signatures

keyof와 T[K]는 문자열 인덱스 시그니처와 상호 작용합니다. 문자열 인덱스 시그니처를 가진 타입을 가지고 있다면, keyof T는 단지 문자열일 것입니다. 그리고 T[string]은 단지 인덱스 시그니처 타입입니다.

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

Mapped types

일반적인 작업은 기존타입을 가져와서 각 프로퍼티를 선택적으로 만드는 것입니다.

interface PersonPartial {
    name?: string;
    age?: number;
}

또는 읽기전용 버전을 원할 수도 있습니다.

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

이것은 JavaScript에서 종종 자주 발생합니다. TypeScript는 이전 타입의 Mapped type을 기반으로 새로운 타입을 생성할 수 있는 방법을 제공합니다. Mapped type에서 새 타입은 이전 타입의 각 특성을 동일한 방식으로 변환합니다. 예를 들어 readonly 또는 optional타입의 모든 프로퍼티를 만들 수 있습니다. 다음은 몇 가지 예입니다.

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

그리고 사용하려면

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

가장 단순한 Mapped type과 그 부분을 살펴 보겠습니다.

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

구문은 내부에 for..in이 있는 인덱스 시그니처의 구문과 유사합니다. 세 부분으로 나뉩니다.

타입 변수 K는 차례대로 각 프로퍼티에 바인딩됩니다. 반복 처리할 프로퍼티의 이름이 들어있는 문자열 리터럴 Union Keys입니다. 프로퍼티의 결과 타입 이 간단한 예제에서 Keys는 하드코딩된 프로퍼티 이름 목록이고 프로퍼티 타입은 항상 boolean이므로 이 Mapped type은 다음과 같습니다.

type Flags = {
    option1: boolean;
    option2: boolean;
}

그러나 실제 응용 프로그램은 위의 Readonly 또는 Partial 처럼 보입니다. 그들은 기존의 타입을 기반으로하며, 어떤 방식으로든 필드를 변형합니다. 그것은 keyof와 indexed access type이 들어있는 곳입니다.

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

그러나 일반적인 버전을 사용하는 것이 더 유용할 수도 있습니다.

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

이 예제들에서, 프로퍼티 리스트는 keyof T이고 결과 타입은 T[P]의 변형입니다. 이것은 Mapped type의 일반적인 사용을 위한 좋은 템플릿입니다. 왜냐하면 이러한 종류의 변환은 Homomorphic이기 때문에 매핑은 T의 프로퍼티에만 적용되고 다른 프로퍼티는 적용되지 않습니다. 컴파일러는 새로운 프로퍼티를 추가하기 전에 모든 기존 프로퍼티 modifier를 복사할 수 있음을 알고 있습니다. 예를 들어, Person.name이 읽기 전용이면, Partial<Person>.name은 읽기 전용이고 선택적입니다.

다음은 T [P]가 Proxy <T>클래스에 싸여있는 또 하나의 예입니다.

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

Readonly <T>와 Partial <T>는 매우 유용하며, Pick와 Record와 함께 TypeScript의 표준 라이브러리에 포함되어 있습니다.

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string | number, T> = {
    [P in K]: T;
}

Readonly, Partial과 Pick은 Homomorphic이고 Record는 그렇지 않습니다. Record가 Homomorphic이 아닌 이유는 프로퍼티를 복사하는 입력 타입을 취하지 않는다는 것입니다.

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

Non-homomorphic 타입은 본질적으로 새로운 속성을 생성하므로 아무 곳에서나 프로퍼티 modifier를 복사할 수 없습니다.

Inference from mapped types(Mapped type의 추론)

이제 타입의 프로퍼티를 Wrapping하는 방법을 알았으므로 다음으로해야 할 일은 Unwrapping하는 것입니다. 다행히도, 그것은 꽤 쉽습니다.

function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}
let originalProps = unproxify(proxyProps);

이 Unwrapping 추론은 Homomorphic Mapped type에서만 작동합니다. Wrapping된 타입이 Homomorphic이 아닌 경우에는 Unwrapping 함수에 명시적 타입 파라미터를 지정해야합니다.

Conditional Types

TypeScript 2.8에서 non-uniform 타입 매핑을 표현하는 기능을 추가하는 conditional 타입이 도입되었습니다. conditional 타입은 타입 관계 테스트로 표현된 조건에 따라 두 가지 타입 중 하나를 선택합니다.

T extends U ? X : Y

위의 타입은 T가 U에 할당 가능할 때 타입이 X 인 경우를 의미하고, 그렇지 않으면 타입이 Y입니다.

조건부 타입 T extends U ? X : Y는 X 또는 Y로 해석되거나, 조건이 하나 이상의 유형 변수에 따라 다르기 때문에 지연됩니다. T 또는 U에 타입 변수가 포함되어있을 때 X 또는 Y로 해석할지 지연할지 여부는 타입 시스템에 T가 항상 U에 할당 가능하다고 결론을 내는 데 충분한 정보가 있는지 여부에 따라 결정됩니다.

즉시 해결되는 일부 타입의 예로서 다음 예를 살펴볼 수 있습니다.

declare function f<T extends boolean>(x: T): T extends true ? string : number;
 
// Type is 'string | number'
let x = f(Math.random() < 0.5)

또 다른 예는 중첩 된 조건부 유형을 사용하는 TypeName 타입 alias입니다.

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";
 
type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"

그러나 조건 타입이 지연되는 장소의 예로서 - 분기를 선택하는 대신 주변에 머물러있는 곳은 다음과 같습니다.

interface Foo {
    propA: boolean;
    propB: boolean;
}
 
declare function f<T>(x: T): T extends Foo ? string : number;
 
function foo<U>(x: U) {
    // Has type 'U extends Foo ? string : number'
    let a = f(x);
 
    // This assignment is allowed though!
    let b: string | number = a;
}

변수 a는 분기를 아직 선택하지 않은 조건 타입을 가집니다. 다른 코드가 foo를 호출하면 U에서 다른 형식으로 대체되고 TypeScript는 조건부 타입을 다시 평가하여 실제로 분기를 선택할 수 있는지 여부를 결정합니다.

한편, 조건부의 각 분기가 해당 대상에 할당 가능한한 다른 대상 타입에 조건부 타입을 할당할 수 있습니다. 위의 예에서 U extends Foo ? string : numberstring | number로 할당 할 수 있습니다. 조건부 결과가 무엇이든 관계없이 문자열 또는 숫자로 알려져 있다.

Distributive conditional types

확인된 타입이 기본 타입 매개변수인 조건부 타입을 분배 조건 타입이라고 합니다. 분산 조건부 타입은 인스턴스화중 union 타입에 자동으로 분배됩니다. 예를 들어, T extends U ? X : Y를 인스턴스화에서 T에 대해서 A | B | C를 타입 매개변수로 되었을 경우 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)로 해석된다.

type T10 = TypeName<string | (() => void))>; // "string" | "function"
type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>; // "object"

분포 조건부 타입의 인스턴스화에서 T extends U? X : Y 조건부 타입 내의 T에 대한 참조는 union 타입의 개별 구성 요소로 해석됩니다 (즉, T는 조건부 타입이 통합 타입에 분산된 후 개별 구성 요소를 참조 함). 또한, X 내의 T에 대한 참조는 추가적인 타입 파라미터 제약 U를 갖는다 (즉, T는 X 내에서 U에 할당 가능하다고 고려된다).

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
 
type T20 = Boxed<string>;  // BoxedValue<string>;
type T21 = Boxed<number[]>;  // BoxedArray<number>;
type T22 = Boxed<string | number[]>;  // BoxedValue<string> | BoxedArray<number>;

T는 Boxed<T>의 실제 분기 내에서 any[]를 추가로 가질 수 있으므로 배열의 요소 유형을 T[number]로 참조 할 수 있습니다. 또한 마지막 예제에서 조건 타입이 union 타입에 비해 어떻게 분산되어 있는지 확인하십시오.

조건부 타입의 분배적 특성은 편리하게 union 타입을 필터링하는 데 사용할 수 있습니다.

type Diff<T, U> = T extends U ? never : T;  // Remove types from T that are assignable to U
type Filter<T, U> = T extends U ? T : never;  // Remove types from T that are not assignable to U
 
type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
type T32 = Diff<string | number | (() => void), Function>;  // string | number
type T33 = Filter<string | number | (() => void), Function>;  // () => void
 
type NonNullable<T> = Diff<T, null | undefined>;  // Remove null and undefined from T
 
type T34 = NonNullable<string | number | undefined>;  // string | number
type T35 = NonNullable<string | string[] | null | undefined>;  // string | string[]
 
function f1<T>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
}
 
function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
    let s1: string = x;  // Error
    let s2: string = y;  // Ok
}

조건부 타입은 매핑 된 타입과 결합 할 때 특히 유용합니다.

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
 
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
 
interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string): void;
}
 
type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T41 = NonFunctionPropertyNames<Part>;  // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>;  // { id: number, name: string, subparts: Part[] }

union 및 intersection 타입과 마찬가지로 조건부 타입은 순환 적으로 참조 할 수 없습니다. 예를 들어, 다음은 오류입니다.

type ElementType<T> = T extends any[] ? ElementType<T[number]> : T;  // Error

Type inference in conditional types

조건부 타입의 extends절 내에서 추론 할 타입 변수를 소개하는 infer선언을 사용할 수 있습니다. 이러한 추론 된 타입 변수는 조건부 타입의 실제 분기에서 참조 될 수 있습니다. 동일한 타입 변수에 대해 여러 개의 추론 위치를 가질 수 있습니다.

예를 들어, 다음은 함수 타입의 반환 타입을 추출합니다.

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

조건부 타입은 순서대로 평가되는 일련의 패턴 일치를 형성하기 위해 중첩 될 수 있습니다.

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;
 
type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

다음 예제는 같은 타입의 변수에 대한 여러 후보가 함께 타입 된 위치에서 유추 될 타입을 생성하는 방법을 보여줍니다.

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

마찬가지로, 유사 형 위치에서 동일한 타입 변수에 대한 여러 후보가 intersection 타입을 추론합니다.

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

오버로드 된 함수의 타입과 같은 여러 호출 시그니처가있는 타입에서 추론 할 때 추론은 마지막 서명 (아마도 가장 관대 한 catch-all 경우)에서 이루어집니다. 인수 타입 목록을 기반으로 과부하 해결을 수행 할 수 없습니다.

declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
type T30 = ReturnType<typeof foo>;  // string | number

일반 타입 매개 변수에 대한 제약 조건 절에서 infer선언을 사용할 수 없습니다.

type ReturnType<T extends (...args: any[]) => infer R> = R;  // Error, not supported

그러나 제약 조건에서 타입 변수를 지우고 대신 조건 타입을 지정하면 대부분 동일한 효과를 얻을 수 있습니다.

type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;

Predefined conditional types

TypeScript 2.8에서 lib.d.ts에 몇가지 미리정의된 조건부 타입을 추가했다.

  • Exclude<T, U> - T에서 U에 할당 할 수있는 유형을 제외합니다.
  • Extract<T, U> - T에서 추출하여 U에 할당 할 수있는 유형을 추출합니다.
  • NonNullable<T> - T에서 null 및 undefined를 제외합니다.
  • ReturnType<T> - 함수 유형의 반환 유형을 얻습니다.
  • InstanceType<T> - 생성자 함수 유형의 인스턴스 유형을 얻습니다.

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
 
type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void
 
type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]
 
function f1(s: string) {
    return { a: 1, b: s };
}
 
class C {
    x = 0;
    y = 0;
}
 
type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // never
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error
 
type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // never
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

Note: Exclude 타입은 여기에 제안된 Diff 타입의 적절한 구현입니다. Diff를 정의하는 기존 코드가 손상되지 않도록 Exclude라는 이름을 사용했습니다. 더해서 그 명명이 타입의 의미를 더 잘 전달한다고 생각합니다.

typescript/advanced_type.txt · 마지막으로 수정됨: 2025/04/15 10:05 저자 127.0.0.1