-
[TS] TypeScript 기초 - 3programing/Language 2020. 1. 26. 19:57
안녕하세요, Einere입니다.
(ADblockd을 꺼주시면 감사하겠습니다.)
2020/01/24 - [programing/JavaScript] - [TS] TypeScript 기초 - 1
2020/01/25 - [programing/JavaScript] - [TS] TypeScript 기초 - 2
2020/01/26 - [programing/JavaScript] - [TS] TypeScript 기초 - 3
2020/01/28 - [programing/JavaScript] - [TS] TypeScript 기초 - 4
2020/01/31 - [programing/JavaScript] - [TS] TypeScript 기초 - 5
2020/02/01 - [programing/JavaScript] - [TS] TypeScript 기초 - 6
이번 포스트에서는 타입스크립트 핸드북을 간단하게 정리한 글입니다.
한글로 번역된 좋은 페이지도 있으므로, 글 하단에 링크 남겨두겠습니다.
Interface
인터페이스는 특정 자료형의 구조를 나타냅니다. (난해하지만, 자바스럽게 말하자면 필드와 메서드를 가질 수 있습니다.)
public, private, protected같은 접근 지정자를 지정할 수 없습니다. (인터페이스의 특성상 무조건 public입니다.)
readonly라는 읽기전용 지정자를 지정할 수 있습니다.
인터페이스는 다른 인터페이스와 클래스를 extrends할 수 있으며, 다중상속이 가능합니다. (제 생각에 인터페이스가 클래스를 상속가능한 이유는, 인터페이스를 자료형으로 취급하기 때문인 것 같습니다.)
interface LabeledValue { // filed label: string; // method getLabel(): void; }
Optional Properties
인터페이스의 모든 속성이 반드시 필요하지는 않을 수 있습니다.
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare = {color: "white", area: 100}; if (config.clor) { // Error: Property 'clor' does not exist on type 'SquareConfig' newSquare.color = config.clor; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } let mySquare = createSquare({color: "black"});
선택적 속성의 장점은 가능한 속성을 명시하면서 인터페이스에 포함되지 않은 속성의 사용을 방지 할 수 있다는 것입니다.
Readonly Properties
interface Point { readonly x: number; readonly y: number; }
인터페이스에는 읽기전용 속성도 추가할 수 있습니다.
공식 문서에 따르면, 속성은 readonly 키워드를 사용하고 변수는 const 키워드를 사용하라고 합니다.
Function Types
JS는 함수도 값으로 평가하기 때문에, 함수도 자료형을 가질 수 있습니다. 따라서, 함수에도 인터페이스를 적용할 수 있습니다.
interface SearchFunc { (source: string, subString: string): boolean; } // 함수를 정의할 때 인터페이스 명세를 따르는지 확인하지 않습니다. let mySearch: SearchFun = function(src, sub) { let result = src.search(sub); return result > -1; } // 함수를 호출할 때 인터페이스 명세를 따르는지 확인합니다. // Argument of type '10' is not assignable to parameter of type 'string'. mySearch(10, {});
함수의 자료형으로 인터페이스를 지정하는 경우, 함수를 정의할 때는 명세를 만족하는지 확인하지 않고 함수를 호출할 때 명세를 만족하는지 확인합니다.
개인적으로는 같은 명세를 가진 함수를 여러개 정의할 일이 많지는 않은 것 같아, 효용성이 있는지 의문이긴 합니다..ㅎㅎ
Indexable Type
인덱서블 타입이란, 인덱싱이 가능한 자료형을 의미합니다. 이것은 특정 자료형의 변수를 배열 혹은 딕셔너리로 구현할 때 유용합니다. 이러한 인덱서블 타입은 index signature를 가져야 하며, 인덱스 시그니쳐는 number 혹은 string이어야 합니다.
// numeric indexer interface StringArray { [index: number]: string; } const myArray: StringArray = ["Bob", "Fred"]; const myStr: string = myArray[0]; // string indexer interface Dictionary { [index: string]: string; } const myDictionary: Dictionary = { a: 'apple', b: 'banana' }; const apple = myDictionary['a']; const banana = myDictionary['b'];
뉴머릭 인덱스 시그니쳐와 스트링 인덱스 시그니쳐 두개를 동시에 가질수도 있습니다. 그러나 반드시 뉴머릭 인덱서의 반환값의 자료형은 스트링 인덱서의 반환값의 자료형에 할당가능해야 합니다.
class Animal { name: string; } class Dog extends Animal { breed: string; } // Numeric index type 'Animal' is not assignable to string index type 'Dog'. interface NotOkay { [x: number]: Animal; [x: string]: Dog; }
이런 제약이 필요한 이유는, 뉴머릭 인덱싱을 한 결과값과 스트링 인덱싱을 한 결과값이 동일함을 보장하는, 일관성이 필요하기 때문입니다.
그런데 JS에서는 인덱싱을 할 때 자동으로 문자열로 변환합니다.
따라서, 스트링 인덱싱을 한 결과 집합은 뉴머릭 인덱싱을 한 결과 집합보다 같거나 커야 합니다. 즉, 스트링 인덱서의 반환값의 자료형은 뉴머릭 인덱서의 반환값의 타입과 동치이거나 상위집합이어야 합니다.
interface Foo{ [index: string]: boolean; // Property 'foo' of type 'string' is not assignable to string index type 'boolean'. foo: string; // Property 'bar' of type 'number' is not assignable to string index type 'boolean'. bar: number; baz: boolean; }
유사하게, 인덱서블 타입이 인덱서를 제외한 속성을 가지는 경우, 해당 속성의 자료형은 반드시 인덱서의 자료형에 할당가능해야 합니다.
Class
자바의 클래스와 유사합니다.
속성을 생성자에서 초기화 하지 않고, 시그니쳐로 초기화 할 수 있습니다.
속성에 public, private, protected와 같은 접근 지정자를 지정할 수 있습니다.
readonly라는 읽기전용 지정자를 지정할 수 있습니다.
클래스는 다른 클래스를 하나만 extends할 수 있고, 다른 여러 인터페이스들을 implements할 수 있습니다.
class Control { private state: any; protected someState: any = 100; } interface SelectableControl extends Control { select(): void; }
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) { } }
static side vs instance side
클래스는 static side 타입과 instance side 타입, 두가지 타입이 있습니다.
interface ClockConstructor { new (hour: number, minute: number); } // 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; constructor(h: number, m: number) { } }
클래스가 인터페이스를 implements할 때, 클래스의 instance side만 체크됩니다. 클래스의 constructor는 static side이기 때문에, 체크되지 않습니다. 라고 합니다. (뭐래는거야)
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 tock"); } } let digital = createClock(DigitalClock, 12, 17); let analog = createClock(AnalogClock, 7, 32);
따라서 위 코드와 같이 static side와 instance side를 분리하는게 좋다고 합니다.
위 코드를 더 간단히 하면 다음과 같습니다.
interface ClockConstructor { new (hour: number, minute: number); } interface ClockInterface { tick(); } const Clock: ClockConstructor = class Clock implements ClockInterface { constructor(h: number, m: number) {} tick() { console.log("beep beep"); } }
즉, ClockConstructor형 변수인 Clock에 ClockInterface를 implements한 Clock이라는 클래스를 할당하는 것입니다. (난해하네요..)
개인적으로는 굳이 생성자 함수를 interface에 집어넣는 이유를 모르겠습니다.. 인터페이스가 클래스를 상속하는것도 이해안되고..
Access Modifiers
자바와 유사하게 클래스는 public, private, protected 접근 지정자를 지정할 수 있습니다.
- public : 기본값입니다. 클래스 외부에서 접근이 가능합니다.
- private : 클래스 내부에서만 접근이 가능하며, 클래스 외부에서 접근이 불가능합니다.
- protected : 해당 클래스 내부와, 해당 클래스를 상속받은 클래스 내부에서 접근이 가능합니다.
class Foo { public publicState: any; private privateState: any; protected protectedState: any; // TS 3.8 이상부터 지원하는 문법. private과 동일하다. #anotherPrivateState: any; }
#을 이용한 private 필드 선언은 ES 2020에 제안이 되었다고 합니다. (참고 : https://d2.naver.com/helloworld/4007447)
공식문서에 따르면, 각 private 필드 간 고립을 더 잘 보장해준다고 합니다. (그런데 현재 최신 TS 버전은 3.7.2...)
compare class type
TS에서 두 자료형을 비교할 때, 자료형의 모든 필드가 비교가능하다면 두 자료형을 비교가능하다고 합니다.
그러나 비교할 필드에 private나 protected가 있다면 다른 자료형으로 판단합니다. (즉, 서로 다른 자료형이므로 비교가 불가능 합니다.)
만약 private나 protected가 포함된 자료형을 서로 비교하고 싶다면, 해당 필드는 같은 선언으로부터 유래해야 합니다.
class Animal { private name: string; constructor(theName: string) { this.name = theName; } } class Rhino extends Animal { constructor() { super("Rhino"); } } class Employee { private name: string; constructor(theName: string) { this.name = theName; } } let animal = new Animal("Goat"); let rhino = new Rhino(); let employee = new Employee("Bob"); animal = rhino; animal = employee; // Error: 'Animal' and 'Employee' are not compatible
Animal형 animal과 Rhino형 rhino는 private name필드를 가지고 있으며, 둘은 Animal 클래스에서 유래했습니다. 따라서 같은 자료형으로 취급되어 비교가 가능해지고, 대입이 가능해집니다.
그러나 Employee형 employee는 private name필드를 가지고 있지만, 같은 선언으로부터 유래한 것이 아니므로 다른 자료형으로 취급됩니다. 따라서 비교가 불가능해지며 대입도 불가능합니다.
Parameter Properties
class Octopus1 { readonly name: string; readonly numberOfLegs: number = 8; constructor (theName: string) { this.name = theName; } } const octopus1 = new Octopus1('무너'); console.log(octopus1.name); // '무너' class Octopus2 { readonly numberOfLegs: number = 8; constructor(readonly name: string) { } } const octopus2 = new Octopus2('무너무너'); console.log(octopus2.name); // '무너무너'
필드는 생성자에서 초기화되는게 보통이지만, 아래와 같이 생성자의 파라미터를 바로 필드로 사용할 수 있습니다.
이런 파라미터 속성은 생성자 파라미터변수 앞에 접근 지정자 혹은 읽기전용 지정자가 명시되어야 합니다.
Accessors
JS와 유사하게, 필드에 대한 setter와 getter를 설정할 수 있습니다.
class Employee { private fullNameMaxLength: number = 10; private _fullName: string; get fullName(): string { return this._fullName; } set fullName(newName: string) { if (newName && newName.length > this.fullNameMaxLength) { throw new Error("fullName has a max length of " + this.fullNameMaxLength); } this._fullName = newName; } } let employee = new Employee(); employee.fullName = "Bob Smith"; if (employee.fullName) { console.log(employee.fullName); }
그러나 접근자를 사용하기 위해서는 두가지 주의사항이 있습니다.
- 컴파일 결과(target)이 ES5 이상이어야 합니다.
- getter만 설정되고 setter는 설정되지 않은 경우, readonly로 간주합니다.
Static Properties
class Grid { static origin = {x: 0, y: 0}; constructor (public scale: number) { } calculateDistanceFromOrigin(point: {x: number; y: number;}) { let xDist = (point.x - Grid.origin.x); let yDist = (point.y - Grid.origin.y); return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale; } } let grid1 = new Grid(1.0); // 1x scale let grid2 = new Grid(5.0); // 5x scale console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10})); console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
스태틱 프로퍼티는 각 인스턴스가 고유하게 가져야 할 값이 아니라, 클래스가 가져야 할 (모든 인스턴스가 동일한 값을 가져야 할) 값을 정의하기 위해 사용합니다.
스태틱 프로퍼티에 접근하기 위해서는 this가 아니라, 클래스명을 써야 합니다.
Abstract Class
인터페이스는 명세(시그니처)를 정의할 뿐, 상세한 구현을 정의하지는 않습니다. 대신 자신을 상속할 클래스에게 구현을 위임합니다.
그러나 가끔 인터페이스처럼 명세도 정의하고 클래스처럼 자세한 로직을 정의하고 싶을 때가 있습니다. 이런 경우 사용하는 것이 추상 클래스입니다. 즉, 인터페이스와 클래스의 중간개념이라고 이해하시면 좋을 듯 합니다.
abstract class Animal { // 명세를 정의 abstract makeSound(): void; // 자세한 로직을 구현 move(): void { console.log("roaming the earth..."); } }
추상 클래스는 기본적으로 클래스이므로 다른 클래스가 extends할 수 있는 등, 클래스의 기본 기능을 가집니다. 추가로, 인터페이스처럼 new 연산자를 이용해 인스턴스를 만들 수 없습니다.
참고
https://www.typescriptlang.org/docs/handbook/interfaces.html
https://www.typescriptlang.org/docs/handbook/classes.html
https://typescript-kr.github.io/
'programing > Language' 카테고리의 다른 글
[TS] TypeScript 기초 - 5 (0) 2020.01.31 [TS] TypeScript 기초 - 4 (0) 2020.01.28 [TS] TypeScript 기초 - 2 (0) 2020.01.25 [TS] TypeScript 기초 - 1 (0) 2020.01.24 [JS] 객체에 관하여 (0) 2020.01.01 댓글