Typescript Handbook / Developer’s Record 를 참고했습니다.

기본

interface LabelledValue {
  label: string;
}

let myObj: LabelledValue = {
  label: "A"
}

let myObj2: LabelledValue = {
  label: "B",
  size: 10 // error : Object literal may only specify known properties, and 'size' does not exist in type 'LabelledValue'
}

Object에 바로 Interface 지정해서 쓰는거는 모양이 딱 맞아야 한다.

하지만, 함수의 인자값으로 들어갈때는 모양이 조금 안맞아도 된다.

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  return labelledObj.label;
}

let myObj = {
  size: 10,
  label: "A"
}

printLabel(myObj) // A

그런데 또…인자값에서 바로 object를 넣는거는 모양이 딱 맞아야 한다.으이구.

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  return labelledObj.label;
}

printLabel({size: 10,label: "A"}) // Object literal may only specify known properties, and 'size' does not exist in type 'LabelledValue'.

왜 이런 혼란스러운 규칙이 있는지 모르겠다. 그냥, 모양이 딱 맞아야 함수의 인자값으로 넣을 수 있다고 생각하는게 맘 편하겠다.

Optional

interface SquareConfig {
  color?: string,
  width?: number
}

function createSquare(config: SquareConfig): {color: string, area: number} {
  let newSquare = {
    color: "white",
    area: 10
  }
  if ( config.color ) {
    newSquare.color = config.color;
  }
  if ( config.width ) {
    newSquare.area = config.width;
  }
  return newSquare;
}

let config: SquareConfig = {
  color: "black"
}

createSquare(config); // { color: 'black', area: 10 }

아니면 3항 연산자를 써도 된다.

interface SquareConfig {
  color?: string,
  width?: number
}

function createSquare(config: SquareConfig) : {color: string, area: number} {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 100
  }
}
console.log(createSquare({color: "black"}))
console.log(createSquare({color: "black", width: 40}))

Excess Property Chekces

interface SquareConfig {
  color?: string,
  width?: number
}

function createSquare(config: SquareConfig) : {color: string, area: number} {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 100
  }
}
createSquare({color: "black", opacity: 0.4}) // Argument of type '{ color: string; opacity: number; }' is not assignable to parameter of type 'SquareConfig'

위처럼 지정된 인터페이스에 맞지 않는 모양의 객체가 인자값으로 들어가면, 당연히 에러가 생기지만, type assertion을 사용하면 해결된다.

interface SquareConfig {
  color?: string,
  width?: number
}

function createSquare(config: SquareConfig) : {color: string, area: number} {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 100
  }
}
createSquare({color: "black", opacity: 0.4} as SquareConfig) // Argument of type '{ color: string; opacity: number; }' is not assignable to parameter of type 'SquareConfig'

왜냐하면! SquareConfig의 property중 어느것도 꼭 필요한게 아니라 optional이기 때문이다. 즉 {color: "black", opacity: 0.4}SquareConfig의 최소조건에 만족한다. 하지만, 더 스마트한 방법은 다음과 같다.

interface SquareConfig {
  color?: string,
  width?: number,
  [myProperty: string]: any
}

[myProperty: string]: any같은거를 string index signature라고 부른다. “프로퍼티의 key는 string이어야 하며 그 value는 any이다” 라는 의미이다.

만약에 이렇게 SquareConfig처럼 뭐가 추가될지 모르는 상황이라서 string index signature를 사용했다면, 한번더 제대로 코딩을 하고 있는건지 점검해봐야한다. 가급적 저렇게 애매한것들, any같은 느낌의 것들은 사용하지 않는게 좋기 때문이다.

Read only

interface Point {
  readonly x: number,
  readonly y: number
}

let p1: Point = {
  x: 10,
  y: 20
}

p1.x = 5 // Cannot assign to 'x' because it is a read-only property

ReadonlyArray<T>

let roa: ReadonlyArray<number> = [1,2,3,4];
roa[1] = 12; // error : Index signature in type 'readonly number[]' only permits reading.

number[] 로 재할당 가능하다.

let roa: ReadonlyArray<number> = [1,2,3,4];
let a: number[] = roa as number[];
a[1] = 100;
console.log(a); // [ 1, 100, 3, 4 ]

readonly vs const

readonly : property에 붙음

const : 변수에 붙음

함수타입

interface SearchFunc {
  (source: string, substring: string): boolean;
}

let mySearch: SearchFunc = function(source: string, substring: string): boolean {
  let result = source.search(substring);
  return result > -1;
}

하지만, 꼭 parameter이름이 같을 필요는 없다.

interface SearchFunc {
  (source: string, substring: string): boolean;
}

let mySearch: SearchFunc = function(s: string, sub: string): boolean {
  let result = s.search(sub);
  return result > -1;
}

그리고 Type도 굳이 안적어 줘도 된다.

interface SearchFunc {
  (source: string, substring: string): boolean;
}

let mySearch: SearchFunc = function(s, sub): boolean {
  let result = s.search(sub);
  return result > -1;
}

Indexable

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray = ["Bob", "Fred"];

console.log(myArray[0]);

하지만, [number]와 [string]이 공존할 수는 없다.

interface Animal {
  name: string = "라쿤"
}

interface Dog extends Animal {
  breed: string;
}

interface NotOkay {
  [x: number]: Animal // Numeric index type 'Animal' is not assignable to string index type 'Dog'.
  [y: string]: Dog;
}

let arr: NotOkay = {
  1: new Animal(),
  2: new Animal(),
  "1": new Dog(),
  "2": new Dog()
}

arr[1]을 했을때 자바스크립트는 내부적으로 arr객체에 접근하기 위해서 1을 "1"로 바꾼다. 그래서 1로 접근했을때 Animal을 받는데 [y: string]: Dog이고 1"1"로 바뀌니까 Dog를 받는다. AnimalDogassignable 하지 않기 때문에 에러가 나는것 같다.

리턴 타입도 겹치면 안된다.

interface NumberDictionary {
  [index: string]: number
  length: number,
  name: string // Property 'name' of type 'string' is not assignable to string index type 'number'.
}

name과 [index: string]의 리턴타입이 달라서 에러가 난다. 물론 Union을 쓰면 해결 가능하긴 하다.

interface NumberDictionary {
  [index: string]: number | string
  length: number,
  name: string
}

readonly 도 가능하다

interface ReadOnlyStringArray {
  readonly [index: number]: string
}
var arr: ReadOnlyStringArray = ["Alice", "Bob", "Chicken"]
arr[1] = "Yoon" // Index signature in type 'ReadOnlyStringArray' only permits reading.

클래스 타입

interface ClockInterface {
  currentTime: Date;
}

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

method도 강제할 수 있다.

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

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

Constructor도 강제 할 수 있다.

interface ClockInterface {
  tick(): void;
}

interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface
}

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 tock tick tock");
  }
}

let ditialC = createClock(DigitalClock, 2, 40);
let analogC = createClock(AnalogClock, 2, 40);
ditialC.tick(); // beep beep
analogC.tick(); // tick tock tick tock

DigitalClockcreateClock함수의 인자값으로 넣기 위해서는 constructor의 모양이 ClockConstructor와 딱 맞아야 한다. 근데 꼭 이런식으로 빙빙 돌아가는 느낌으로 만들어야 할까? createClock이 필요할까? constructor함수의 모양을 강제하면서 바로 new를 써버리는건 안되는걸까?

interface ClockConstructor {
  new (hour: number, minute: number): any;
}

// Class 'Clock' incorrectly implements interface 'ClockConstructor'
// Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.
class Clock implements ClockConstructor {
  currentTime: Date = new Date();
  constructor(h: number, m: number) {}
}

위의 코드는 잘 작동할것 같지만, 실제로는 에러를 내뿜는다. 이에 관한 설명이 있는데 나는 읽어도 잘 모르겠다.

Extends

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square: Square = {
  color: "black",
  sideLength: 100
};

다중 extends도 가능하다.

interface Shape {
  color: string;
}

interface PenStrock {
  penWidth: number;
}

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

let square: Square = {
  color: "black",
  sideLength: 100,
  penWidth: 50
};

이런걸 어디다 쓰지…?

Hybrid(짬뽕)

interface Counter {
  (start: number): string,
  interval: number,
  reset(): void
}

function getCounter(): Counter {
  let counter = <Counter>function(start: number) { console.log(start) };
  counter.interval = 100;
  counter.reset = function() {};
  return counter;
}

let counter: Counter = getCounter();
counter(10) // 10
counter.reset();
counter.interval = 5.0

function이 함수이면서 Object이기 때문에 가능한것이다. 재미있는 점은, (start: number): string에 key가 없다는것! 그리고, 이상한점은,,,(start: number) : string 인데 let counter = <Counter...에서는 리턴을 안했다. 이상하네,, 뭔가 딱 안맞는 느낌이다.

Interfaces Extending Classes

class Control {
  private state: any;
}

interface SelectableControl extends Control {
  select(): void;
}

class Button extends Control implements SelectableControl {
  select() {}
}

class TextBox extends Control {
  select() {}
}

// Class 'ImageControl' incorrectly implements interface 'SelectableControl'.
// Types have separate declarations of a private property 'state'.
class ImageControl implements SelectableControl {
  private state: any;
  select() {}
}

무슨말인지 모르겠다.