TypeScript Study Note

Ginie·2021년 1월 11일
0

TypeScript

목록 보기
3/11
post-thumbnail

타입 구성 (Composing Types)

객체들을 조합하여 더 크고 복잡한 객체를 만드는 방법과 유사하게 TS에 타입으로 이를 수행하는 도구가 있다. 여러가지 타입을 이용하여 새 타입을 작성하기 위해 일상적인 코드에서 가장 많이 사용되는 코드는 유니언과 제네릭이다.

유니언 (Unions)

유니언은 타입이 여러 타입 중 하나일 수 있다는 것을 선언한다.
유니언은 다양한 타입을 처리하는 방법을 제공한다. 가장 많이 사용 되는 사례 중 하나는 값이 다음과 같이 허용되는 string 또는 number의 리터럴 집합을 설명하는 것이다.
리터럴 집합
리터럴 타입은 집합 타입의 보다 구체적인 하위 타입. "Hello World"는 string이지만, string은 "Hello World"가 아니다~
리터럴 타입 좁히기
let, var로 선언한건 값이 변할 수 있다고 컴파일러에게 알린다. 하지만 const로 선언한 변수는 변하지 않는다고 알린다.
문자열 리터럴 타입
실제로 문자열 리터럴 타입은 유니언 타입, 타입 가드 그리고 타입 별칭과 잘 결합된다. 이런 기능을 함께 사용하여 문자열로 enum (열거형: 서로 연관된 상수들의 집합) 과 비슷한 형태를 갖출 수 있다. 오버로드를 구별하는 것과 동일한 방법으로 사용될 수 있다.
숫자형 리터럴 타입
TS 에서 위의 문자열 리터럴과 같은 역할을 하는 숫자형 리터럴 타입도 있다.
주로 설정값을 설명할 때 사용.

리터럴 집합 예시보기

 type WindowStates = 'open'|'closed'|'minimized';
 type LockStates = 'locked'|'unlocked';
 type OddNumberUnderTen = 1 | 3 | 5 | 7 | 9;

또는 array, string을 받는 함수가 있을 수 있다.

function getLength(odj: string | string[]) {
    return obj.length;
}

type Mybool = true | false;
 //마우스를 올리면 bool 타입으로 분류된다.

(구조적 타입 시스템의 프로퍼티)

  • 가끔 number 이나 string을 매개변수로 기대하는 라이브러리를 사용할 때가 있다.
function padLeft(value: string, padding: any) {
    // 문자열을 받고 왼쪽에 padding을 추가한다.
    if (typeof padding === 'number') {
        return Array(padding + 1).join(' ') + value;
        // 만약 'padding'이 문자열이라면, 'padding'은 왼쪽에 더해진다.
    }
    if (typeof padding === 'string') {
        return padding + value;
        // 만약 'padding'이 숫자라면 그 숫자만큼 공백이 왼쪽에 더해질 것이다.
    }

    throw new Error(`Expected string or number, got '${padding}'.`)
}

console.log(padLeft('Hello world', 10)); // '          Hello world'출력

declare function padLeft2(value: string, padding: any): string;

let indentedString = padLeft2('Hello world', true); // 컴파일 타임은 통과, 런타임 오류

function padLeft3(value: string, padding: string | number) {
  // ...
} // 여러 타입중 하나가 될 수 있다는 뜻. 값의 타입이 number, string 혹은 boolean이 될 수 있음을 의미

공통 필드를 갖는 유니언

  • 유니언 타입인 값이 있으면 유니언에 있는 모든 타입에 공통인 멤버들에게만 접근이 가능하다.
 interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    latEggs(): void;
}

declare function getSmallPet(): Fish | Bird;

let pet = getSmallPet();
pet.layEggs();

pet.swim();
// 두 개의 잠재적인 타입 중 하나만 사용할 수 있다.
  • 값이 A | B 타입을 갖고 있으면, 확신할 수 있는 것은 그 값이 A와B 둘 다 가지고 있는 멤버들을 갖고 있다는 것뿐이다.

유니언 구별하기

  • 현재 가능한 타입 추론의 범위를 좁혀나가게 해줄 수 있는 리터럴 타입을 갖는 단일 필드를 사용 하는 것이다.
type NetworkLoadingState = {
    state: 'loading';
}

type NetworkFailedState = {
    state: 'failed';
    code: number;
}

type NetworkSuccessState = {
    state: 'success';
    response: {
        title: string;
        duration: number;
        summary: string;
    };
};

type NetworkState = | NetworkLoadingState | NetworkFailedState | NetworkSuccessState;
// 모두 state 라는 필드를 갖고 있다. 
// state 필드가 공통으로 존재한다는 점을 안다면, 존재 여부를 체크하지 않고도 접근할 수 있다. 
  • 리터럴 타입으로서 state를 가지고 있다면, state의 값은 대응하는 동일한 문자열과 대조되고 TS는 현재 어떤 타입이 사용되고 있는지 알 것이다.
    • 이 경우, 런타임에 나타나는 타입의 범위를 좁히기 위하여 switch문을 사용 한다.
    function networkStatus(state: NetworkState): string {
      switch (state.state) {
          case 'loading': return 'Downloading...';
          case 'failed': return `Error ${state.code} downloading`;
          case 'success': return `Downloaded ${state.response.title} = ${state.response.summary}`
      }
    }

교차 타입

  • 여러 타입을 하나로 결합한다. 기존 타입을 합쳐 필요한 모든 기능을 가진 하나의 타입을 얻을 수 있다.
interface ErrorHandling {
    success: boolean;
    error?: { message: string }
}

interface ArtworksData {
    artworks: { title: string }[];
}

interface ArtistsData {
    artists: { name: string }[];
}

type ArtworksResponse = ArtworksData & ErrorHandling;
type ArtistsResponse = ArtistsData & ErrorHandling;

const handleArtistsResponse = (response: ArtistsResponse) => {
    if (response.error) {
        console.log(response.error.message);
        return;
    }
    console.log(response.artists);
};

교차를 통한 믹스인

  • 믹스인 : 프로토 타입을 바꾸지 않고 한 객체의 프로퍼티를 다른 객체에게 복사해 사용하는 방식이다. 기존에 있던 객체의 기능을 그대로 가져가면서 다른 객체에 추가할 때 주로 사용됨.
class Person {
    constructor(public name: string) { }
}

interface Loggable {
    log(name: string): void;
}

class ConsoleLogger implements Loggable {
    log(name: string) {
        console.log(`Hello I'm ${name}.`);
    }
}

// 두 객체를 받아 하나로 합친다.
function extend<First extends {}, Second extends {}>(
    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;
}
const jim = extend(new Person('Jim'), ConsoleLogger.prototype);
jim.log(jim.name);

제네릭 (Generics)

  • 제네릭 : 타입을 불문하고 동작하는 것
    • C#과 Java 같은 언어에서, 재사용 가능한 컴포넌트를 생성하는 도구상자의 주요 도구 중 하나!
    • 다양한 타입에서 작동하는 컴포넌트를 작성 가능!
    • 함수에 인수를 넘길 때 타입 인수도 같이 넘기기 때문에 타입에 관한 어떤 정보도 잃지 않는다.
type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;


(backpack 변수가 string이므로, add 함수에 number를 전달할 수 없음)

  • identity 함수 : 인수로 무엇이 오던 그대로 반환하는 함수 (echo명령과 비슷함)
function identity(arg: number): number {
    return arg;
}
// 제네릭이 없다면 특정 타입을 주어야함
  • 하지만 any 타입을 사용해서 기술 할 수 있는데, 이는 어떤 타입이든 받을 수 있다는 점이 제네릭이지만, 실제 함수가 반환할 때 어떤 타입인지 정보는 잃는다.
  • 대신, 무엇이 반환 되는지 표시하기 위해 인수의 타입을 캡쳐할 방법이 필요함.
  • 값이 아닌 타입에 적용되는 타입 변수를 사용하자.
function identity<T>(arg: T): T {
  return arg;
}
  
// 두 가지 방법으로 호출 할 수 있다.
let output = identity<string>('myString);
// 함수에 타입 인수를 포함한 모든 인수를 전달한다.
console.log(output); //'myString'
let output = identity('myString')
// 전달 하는 인수에 따라 컴파일러가 T의 값을 자동으로 정하게 함(타입 인수 추론) 
  • 타입 인수 넘길 땐 <> 이 괄호안에 표기한다.

타입 인수 추론

  • 제네릭 함수를 호출하는 방법
  • 타입 인수를 넘겨주지 않고 컴파일러가 전달하는 인수를 보고 추론한다.
  • 가독성 있고 간결하지만 컴파일러가 타입을 추론할 수 없는 복잡한 상황에서는 타입을 전달하는 것이 좋음!

제네릭 타입 변수 작업

  • 밑의
    예제
    를 보면 length 메서드를 사용했는데 타입 인수가 number 라면 이는 에러를 일으킨다.
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}
  • 이 코드를 맞게 하려면 function loggingIdentity<T>(arg: T[]): T[] 배열로 만들면 된다.

제네릭 타입

  • 제네릭 함수의 타입 선언하기 👇
function identity<T>(arg: T): T {
    console.log(arg);
    return arg;
}
let myIdnetity: <T>(arg: T) => T = identity;

```typescript
- 타입 매개변수에 다른 이름을 사용 할 수 있다. `let myIdentity: <U>(arg: U) => U = identity;`
- 객체 리터럴 방식 `let myIdentity: { <T>(arg: T): T } = identity;`
- 제네릭 인터페이스 

```typescript
interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;
  • 제네릭 타입 매개변수를 인터페이스의 매개변수로 사용하려면 👇
interface GenericIdentityFn<T> { 
// 달라잔 점! <T>
    (arg: T): T;
}
//...생략...

let myIdentity: GenericIdentityFn<number> = identity; // <타입> 추가

제네릭 클래스

  • 클래스 이름 뒤 <>를 붙이고 안에 타입 매개변수를 전달한다. 클래스 블럭 구문에는 타입 매개변수 목록들을 가진다.
class GenericNumber<T> {
    zeroValue: T,
    add: (x: T, y: T)
        => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; }
  • 클래스는 스태틱인스턴스 타입을 가진다. 제네릭 클래스는 인스턴스의 기준에서만 제네릭이므로, 제네릭을 클래스로 작업할 때 정적 프로퍼티는 클래스의 타입 매개변수를 이용할 수 없다. 클래스 참조 class Static VS Instance

제네릭 제약조건

  • 위의 예제를 다시 보자
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}
  • 만약 length 프로퍼티를 가진 타입으로 제약 조건을 걸고 싶을때?
    • extends 키워드로 인터페이스를 사용하면 된다.
interface Lengthwise {
    length: number;
}
function loggingIndentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length) // 이제 .length 프로퍼티가 있는 것을 알기 때문에 더 이상 오류가 발생하지 않습니다.
    return arg;
}

loggingIndentity({ length: 10, value: 3 });

제네릭 제약 조건에서 타입 매개변수 사용

  • 타른 타입 매개변수로 제한된 타입 매개변수를 선언할 수 있다.
    이름이 있는 객체에서 프로퍼티를 가져오고 싶은 경우, 실수로 obj 에 존재하지 않는 프로퍼티를 가져오지 않게 제약을 건다.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, 'a');
getProperty(x, 'm'); // 오류
  • KT key타입(keyof)으로 제한하므로서 실수를 방지할 수 있다.

제네릭에서 클래스 타입 사용

  • 팩토리 : 객체 생성을 단순하게 하여 반복적인 객체들을 생성할 때, 사용하는 객체 지향 프로그래밍에서 유래된 디자인 패턴
  • 팩토리를 사용할 때 생성자 함수에서 클래스 타입을 참조해야한다.
    예제를 보자.
function create<T>(c: {new(): T; }): T {
  return new c();
}
  • 프로토타입 프로퍼티를 사용하여 생성자 함수와 클래스 타입의 인스턴스 사이의 관계를 유추하고 제한한다.
class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}

createInstance(Lion).keeper.nametag;  // 타입검사!
createInstance(Bee).keeper.hasMask;   // 타입검사!
profile
느리지만 꾸준한 프론트엔드 주니어 개발자 🐢

0개의 댓글