TypeScript Study Note

Ginie·2021년 1월 11일
0

TypeScript

목록 보기
4/11
post-thumbnail

구조적 타이핑

  • TS의 핵심 원칙 중 하나는 타입 검사가 값이 있는 형태에 집중한다는 것이다.
  • '덕 타이핑(duck typing)' '구조적 타이핑(structural subtyping)'이라고 불린다.
  • 두 객체가 같은 형태를 가지면 같은 것으로 간주한다.
interface Point {
    x: number;
    y: number;
}

function printPoint (p: Point) {
    console.log(`${p.x}, ${p.y}`);
}

const point = {x: 12, y: 26}; //여기 변수는 Point 타입으로 선언 된 적이 없지만,
printPoint(point);

//TS는 타입 검사에서 point 와 Point 형태를 비교, 
둘 다 같은 형태라 통과!
  • 형태 일치에서는 일치시킬 객체의 필드의 하위 집합만 필요하다!
  • 객체 또는 클래스에 필요한 모든 속성이 있다면?
  • TS는 구현 세부 정보에 관계 없이 일치하다고 봄.
interface Point { 👈 여기의 속성이랑
    x: number;
    y: number; 
}

function printPoint (p: Point) {
    console.log(`${p.x}, ${p.y}`);
}

class VirtualPoint { 👈 여기의 속성이 같다
    x: number;
    y: number; // 모든 속성이 다 있음!
    constructor(x: number, y: number){
        this.x = x;
        this.y = y;
    }
}

const newVPoint = new VirtualPoint(13, 56);

printPoint (newVPoint);

첫 번째 인터페이스

잘 알려면 예제를 열심히 따라 쳐라

  • 타입들의 이름을 짓는 역할.
  • 코드 안의 계약을 정의.
  • 프로젝트 외부에서 사용하는 코드의 계약을 정의.
  • 함수에 전달된 객체가 나열된 요구 조건을 충족 했는지 안했는지가 중요하다.
function printLabel(labelObj: { label: string }) { // 타입검사는 호출을 확인한다.
  // printLabel는 string 타입 label을 갖는 객체를 매개변수로 갖는다.
  console.log(labelObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
// 컴파일러는 최소 필요한 프로퍼티가 있는지, 타입이 잘 맞는지만 검사를 한다.
printLabel(myObj);

// 👇 똑같은 예제를 interface로 바꿔보자

interface LabelValue { // 요구사항을 기술하는 이름으로 사용 가능
    label:string;
}

function printLabel(labelObj: LabelValue) { 
// 여기에 전달한 객체가 LabelValue 인터페이스를 구현 해야한다고 명시적으로 얘기 할 필요 없다.
  console.log(labelObj.label);
}

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

선택적 프로퍼티 (Optional Properties)

  • 인터페이스의 모든 프로퍼티가 필요한게 아니라 선택적으로 조건을 주고 싶을 때
    • 객체 안의 몇 개의 프로퍼티만 채워 함수에 전달하는 패턴 만들 때 사용
interface SquareConfig {
    color?: string;
    width?: number;
    // 선택적 프로퍼티는 선언할 때 이름 끝에 '?'를 붙여 표시한다.
}
 
function createSquare(config: SquareConfig):{color: string; area: number}{
let newSquare = {color: 'white', area: 100};
 
if (config.color) {
    // 인터페이스에 속하지 않는 프로퍼티 사용을 방지한다
    newSquare.color = config.color;
}
 
if (config.width){
    newSquare.area = config.width * config.width;
}
return newSquare;
}
 
let mySquare = createSquare({color: 'black'});

읽기전용 프로퍼티 (Readonly properties)

  • 객체가 처음 생성될 때만 수정 가능하게 함.
  • 프로퍼티 앞에 readonly를 넣어서 지정한다.
  • 할당 후에는 프로퍼티를 수정 할 수 없다.
  • 생성 후 배열을 변경하지 않게 하는 Array<T>와 동일한 ReadonlyArray<T>타입이 있다.
let a: number[] = [1,2,3,4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // Index signature in type 'readonly number[]' only permits reading.(2542)
ro.push(5); // Property 'push' does not exist on type 'readonly number[]'.(2339)
ro.length = 100; // Cannot assign to 'length' because it is a read-only property.(2540)
a = ro; // The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.(4104)
//👆 ReadonlyArray를 일반 배열에 재할당이 불가능
a = ro as number[]; // ⭕타입 단언(type assertion)으로 오버라이드하는 것은 가능

const VS readonly
변수는 const 프로퍼티는 readonly

초과 프로퍼티 검사 (excess property checking)

interface SquareConfig {
    color?: string;
    width?:number;
}
 
function createSquare(config: SquareConfig): {color: string, area: number}{
    let newSquare = {color: 'white', area: 100};
 
    if(config.color) {
        newSquare.color = config.color;
    }
 
    if(config.width) {
        newSquare.area = config.width * config.width;
    }
 
    return newSquare;
}
 
let mySquare = createSquare({colour: 'black', width: 100}) // 이부분을 colour로 바꿈
// Argument of type '{ colour: string; width: number; }' is not assignable to parameter of type 'SquareConfig'
// Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'
// Did you mean to write 'color'?(2345)

JS 였으면 마지막 줄에서 조용히 오류가 난다.
width는 있고, color는 없고, colour 프로퍼티는 별로 안중요해서 이 프로그램이 올바르게 작성되었다고 생각 할 수도 있어. 하지만 우리의 TS는 용납하지 않지. 객체 리터럴은 다른 변수에 할당할 때, 인수로 전달할 때 특별한 처리를 받고, 초과 프로퍼티 검사를 받는다.

  • 검사를 피하는 방법

    1. 타입을 선언한다.
    let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
    1. 문자열 인덱스 서명 (string index signatuer)을 추가한다.
      SquareConfig에 추가 프로퍼티가 있을거라고 확실하게 예상한다?
    interface SquareConfig {
        color?: string;
        width?:number;[propName : string]:any;
      }  
    1. 객체를 다른 변수에 할당한다. (공통 프로퍼티가 있을 때만 가능!)
    let squareOptions = { colour: "red", width: 100 }; //width가 공통 프로퍼티
    let mySquare = createSquare(squareOptions);
  • 간단하면 위의 방법들은 시도하지 말자.

  • 더 복잡한 객체 리터럴에서 이 방법을 생각해볼 수 있다. 하지만 초과 프로퍼티 에러는 대부분 실제 버그다.

  • 선택적 프로퍼티에서 이런 오류가 난다면 타입 정의를 수정해야 할 필요가 있다.

함수 타입

참조 : 함수

  • 함수의 매개변수와 리턴 값의 타입을 정하는 방법.
interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc; // SearchFunc을 타입으로 갖는 변수

mySearch = function(source: string, subString: string) { 
// 변수는 함수를 할당, string 타입 매개변수 두개랑 bool 타입 값을 리턴
    let result = source.search(subString);
    return result > -1;
}
mySearch = function(source: string, subString: string)
mySearch = function(src: string, sub: string) // 매개변수의 이름이 같을 필요는 없다!
mySearch = function(src, sub) // 타입이 정의되지 않음. 
// 함수의 매개변수는 같은 위치에 대응대는 매개변수와 한 번에 하나씩 검사한다. 
// SearchFunc 타입의 변수로 함수의 매개변수 값이 할당되기 때문에 
// TS의 문맥상 타이핑 (contextual typing)이 자동으로 매개변수의 타입을 추론한다.

인덱서블 (indexable) 타입

  • 인덱서블 타입은 인덱스 시그니처(index signature)를 가지고 있다.
    • 인덱스 시그니처 : 인덱싱 타입과 반환 값의 타입을 기술하는 것.
    • 인덱싱에 지원 되는 타입은 문자열숫자가 있다.
  interface StringArray {
  [index: number]: string; // number로 인덱싱하면 string을 반환한다
  }
  
  let myArray: StringArray;
  myArray = ['BOB', 'FRED'];
  
  let myStr = myArray[0];
  console.log(myStr);
  • 인덱스 시그니처는 프로퍼티 반환타입이 일치하게 한다.
interface NumberDitionary {
  [index: string]: number;
  length: number;
  name: string; // 에러, 반환값이 string으로 맞지 않기때문이지
}
  • 위의 예제를 맞게 하려면 [index: string]: number | string;
  • 인덱스 할당을 막기 위해 인덱스 시크니처를 Readonly 타입으로 만들 수 있다.
interface ReadonlyStringArray {
   readonly [index: number]: string;
}

let myArray: ReadonlyStringArray = ['ali', 'bob'];
myArray[2] = 'mell' // 오류! 읽기 전용이므로 변경 불가!

클래스 타입

인터페이스 구현하기

  • 클래스에서 인터페이스를 구현하려면 implements를 사용한다.
interface ClockInterface {
  currentTime: Date;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  constructor(h: numer, m: number) {
  }
}
  • 메서드도 구현 가능하다.
interface ClockInterface {
    currentTime: Date;
    setTime(d: Date): void;
}

class Clock implements ClockInterface {
    currentTime: Date = new Date();
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number){}
}

class Static VS Instance

  • 클래스틑 두가지 타입을 가진다
    • static : 항상 값이 변하지 않는 변수나 메소드 앞에 붙여 사용한다.
    • instance : 클래스에 의해서 만들어진 객체 Animal cat = new Animal() cat은 객체이고 객체는 Animal의 인스턴스이다. 객체(cat)가 클래스(Animal)의 객체인지를 관계위주로 설명할 때 사용
  • 생성 시그니처로 인터페이스를 생성하고, 클래스를 생성하려고 한다면, 인터페이스를 implements 할 때, 에러가 발생한다.
    • 오류 이유 : 생성자가 static이기 때문에 클래스의 인스턴스만 검사한다.
interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    constructor(h: number, m: number) { }
}
  • 이를 해결 하기 위해 두 가지 방법이 있다.
interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
    tick(): void;
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number){ }
    tick() {
            console.log('beep beep');
    }
    
}

class AnalogClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log('tick tok');
  }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32); 

// createClock 생성자 함수의 첫번째 매개변수 ctor은 
// (hour: number, minute: number)의 생성자로 만들어진 인스턴스이며 이는 
// tick() 메서드를 갖는 ClockInterface 타입
  • 중요한 점은 ClockConstructor 인터페이스를 할당하는 건 인스턴스여야 한다는 점이다.
  1. 클래스 표현을 사용하자.
interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
  tick(): void;
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
};

let clock = new Clock(12, 17);
clock.tick();

인터페이스 확장하기

  • 확장은 한 인터페이스의 멤버를 다른 것에 복사하는 것을 허락한다.
    • 장점: 인터페이스를 재사용성 높은 컴포넌트로 쪼갤 때 유연함을 제공한다.
interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = {} as Square;
square.color = 'blue';
square.sideLength = 10;
  • 인터페이스는 여러 인터페이스를 확장 할 수 있다.
    • 모든 인터페이스의 조합을 만들어 낼 수 있다.
interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = {} as Square;
square.color = 'blue';
square.sideLength = 10;
square.penWidth = 5.0;

하이브리드 타입

  • 함수와 객체 역할을 모두 수용하는 객체
interface Counter {
    (start: number): string; // 매개변수
    interval: number; // 프로퍼티
    reset(): void; // 메서드
}

function getCounter(): Counter {
    let counter = (function (start: number){}) as Counter;
    counter.interval = 123;
    counter.reset = function() { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

클래스를 확장한 인터페이스

  • 인터페이스 타입이 클래스 타입을 확장하면 클래스 멤버는 상속받지만 구현은 상속받지 않는다.
    • 인터페이스가 구현을 제공하지 않고 클래스의 멤버 모두를 선언한 것과 마찬가지
  • privateprotected 멤버도 상속받는다.
    • 인터페이스가 privateprotected 멤버 포함 클래스를 확장할 수 있다는 뜻.
    • 인터페이스 타입은 클래스나 하위클래스에 의해서만 구현될 수 있다.
    • 큰 상속계층을 갖고 있을 때 유용
    • 특정 프로퍼티를 가진 하위클래스에서만 코드가 동작하게 함
    • 하위클래스는 기초클래스에서 상속하는 것 외에 관련 있을 필요 없음.
class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
    // Control를 상속받음
    // Control, 또는 그 밑에 클래스에 의해서만 구현 될 수 있다. 
}

class Button extends Control implements SelectableControl {
    select() { }
    // 얘랑
}

class TextBox extends Control {
    select() { }
    // 얘
}
// 에러!
class Image implements SelectableControl {
    private state: any;
    select() { }
    // 얘는 왜 안되냐? 
    // Control 클래스에 의해 만들어진 state여야만 하는데 (private이기 때문에) 
    // 이 클래스에서 새롭게 정의한 state여서 호환이 안됨.
}
profile
느리지만 꾸준한 프론트엔드 주니어 개발자 🐢

0개의 댓글