header wave

Post

TS) 타입에 대하여 - 2

2022-09-24 PM 04/45
#typescript
#study

1. 커스텀 타입이란?

타입스크립트는, type, interface, enum 키워드 및 클래스 선언으로 커스텀 타입을 만들 수 있다.

type Foot = number;
type Pound = number;

type patient = {
	나이: number;
	키: Foot;
	몸무게: Pound;
}

let patient:Patient = {
	나이: 99,
	키: 190,
// 몸무게 프로퍼티가 없으면 에러가 난다.
}

해당 프로퍼티가 필수가 아닌 옵션 사항이라면, 프로퍼티 이름에 ?를 붙여 조건부 프로퍼티임을 선언한다.

함수 시그니처의 type 키워드와 타입 별핑

type ValidatorFn =
(c: FormControl) => {[key: string]: any} | null

[key: string]: any 는 모든 타입의 프로퍼티를 가질 수 있는 객체를 의미한다.

class FormControl {
	constructor(initialValue: string, validator: ValidatorFn | null){....}
}

2. 클래스 내 커스텀 타입 사용


Background) 클래스와 상속

기본적으로 자바스크립트 객체는 Object에서 상속된다. 객체 상속은 prototype 프로퍼티를 통해 구현된다. 이를 프로토타입 상속이라고 한다.

ES5)


function Tax() {
	// Tax 객체 코드
}

function BSTax() {
	// 부산 택스 객체
}

BS.prototype = new Tax(); // Tax에서 BSTax를 상속받는다.
var bsTax = new BSTax();

ES6)

class Tax{} // 슈퍼 클래스

class BSTax extends Tax{} // 서브클래스

let bsTax = new BSTax();

함수 선언과 다르게, 클래스 선언은 호이스팅 되지 않는다. 클래스 선언 없이 사용하게 되면 ReferenceError가 발생한다.

var tax1 = new Tax(); // Tax의 첫번째 인스턴스
var tax2 = new Tax(); // Tax의 두번째 인스턴스

function Tax() {
	// Tax 클래스 코드 작성
}

Tax.prototype = {
	calcTax: function() {
		// BSTax 세금 객체 작성
	}
}

ES6의 문법

class Tax() {
	calcTax(){
		// 세금계산 코드
	}	
}

생성자

인스턴스화하는 동안, 클래스는 생성자에서 코드를 실행한다. 생성자는 객체가 생성될 때 한 번만 실행되는 특수한 메서드다.

calss Tax {
	constructor(income){
		this.income = income;
	}
}

const myTax = new Tax(50000);

class Tax{
	constructor(income){
		this.income = income;
	}
}

class BSTax extends Tax{
 // 부산 세금 관련 코드를 작성
}

const bsTax = new BSTax(50000);

BSTax 서브 클래스는 자체 생성자를 정의하지 않기 때문에, Tax 슈퍼 클래스의 생성자는 bsTax 인스턴스화 중에 자동으로 호출된다. 서브 클래스가 자체 생성자를 정의한 경우에는 해당되지 않는다.

super 키워드 및 함수

super 함수를 사용하여 서브 클래스(하위)가 슈퍼 클래스(상위)의 생성자를 호출 할 수 있다. super 키워드는 슈퍼 클래스에 정의된 메서드를 호출하는 데 사용된다.

class Tax {
	constructor(income){
		this.income = income;
	}

	calculateFederalTax() {
		console.log(`TAX ~ ${this.income}`)
	}
	
	calcMinTax() {
		console.log(`MIN TAX`);
		return 123;
	}
}

class BSTax extends Tax {
	constructor(income, stateTaxPercent){
	super(income);
	this.stateTaxPercent = stateTaxPercent;
	}
	
	calculateStateTax() {
		console.log(`Calculated State Tax ${this.income}`);
	}
	
	calcMinTax() {
	let minTax = super.calcMinTax();
	console.log(`Min Tax ${minTax}`)
	}
}

const theTax = new BSTax(50000, 6);

theTax.calculateFederalTax();
theTax.calculateStateTax();
theTax.calcMinTax();

NJTax 클래스 생성자는 두 개의 파라미터, income 및 stateTaxPercent가 있으며, 인스턴스화하는 동안 제공된다.

super() 함수를 사용해 슈퍼 클래스의 생성자를 호출한다. Tax 및 BSTax 클래스 모두 calcMinTax() 메서드가 있다. 슈퍼 클래스에 선언된 메서드를 서브 클래스에서 재작성하는 것을 메서드 오버라이딩이라고 한다. 메서드 오버라이딩은 코드 변경 없이 슈퍼 클래스의 메서드 기능을 대체하기 위해 주로 사용된다.

정적 클래스 멤버

여러 클래스 인스턴스에서 공유하는 클래스 프로퍼티가 필요한 경우, static 키워드를 사용해 만들어야 한다.

class A {
	static counter = 0;
	printCounter() {
		console.log("static couter=" + A.counter);
	}
}

const a1 = new A();
A.counter++;
a1.printCounter(); // 1
A.counter++;
const a2 = new A();
a2.printCounter(); // 2

console.log(a1.counter) // undefined
console.log(a2.counter) // undefined

static 키워드를 사용하여 정적 메서드를 만들 수 있다. 정적 메서드는 클래스 인스턴스가 아니라 클래스 자체에서 호출된다. 클래스 유틸리티 함수 모음을 만들 때 주로 정적 메서드를 사용하며 인스턴스화가 필요하지 않다.

class Helper {
	static  convertDollarsToEuros() {
		// convert method
	}
}

Helper.converDollarsToEuros(); // 클래스를 인스턴스화하지 않고 정적 메서드를 호출한다.

타입스크립트는 다른 객체 지향 언어와 같이 접근 제어자가 있으며, readonly, private, protected, public 키워드가 있다.

const p = new Person("힘찬", "김", 29); // 인스턴스의 타입을 명시적으로 적어주지 않아도 타입 추론이 됨

const p:Person = new Person("힘찬", "김", 29); // 타입을 명시적으로 적어줘도 됨

class Block {
	readonly nance:number;
	readonly hash:string;

	constructor(
		readonly index: number,
		readonly previousHash: string,
		readonly timestamp: number,
		readonly data: string,
	){
		const {nance, hash} = this.mine();
		this.nonce = nonce;
		this.hash = hash;
	}
	// 이후 코드 생략
}

인터페이스를 사용한 커스텀 타입

interface Person {
	firstName: string;
	lastName: string;
	age: number;
}

타입스크립트의 구조적 타입 시스템

타입스크립트는 두 타입의 구조만을 가지고 호환성을 결정한다. 서로 다른 타입임에도 멤버가 서로 일치한다면 두 타입은 서로 호환되며, 명시적인 표시는 필요하지 않다.

type, interface, calss를 언제 사용해야 할까?

런타임 동안 객체를 인스턴스화 한다면, interface 또는 type을 사용하고, 그 반대의 경우는 class를 사용한다. 즉 값을 나타내는 데는 class를 사용한다.

타입스크립트의 타입 검사기로 안전하게 커스텀 타입을 선언하고자 한다면 type 또는 interface를 사용한다. 이들 키워드는 자바스크립트 코드로 컴파일 되지 않으므로 런타임 코드 용량이 더 작아지지만, class는 자바스크립트 코드로 컴파일되기 때문에 용량이 커진다.

type 키워드는 interface와 동일한 기능뿐만 아니라 더 많은 기능을 사용할 수 있다. interface는 합집합 또는 교집합 개념을 사용할 수 없지만, type은 사용이 가능하다.

구조적 타입 시스템과 명목적 타입 시스템

// Java

class Person {
	String name;
}

class Customer {
	String name;
}

Customer cust = new Person(); // 구문 오류: 왼쪽과 오른쪽의 클래스 이름이 같지 않습니다.

class Person {
	name: string;
}

class Customer {
	name: string;
}

const cust: Customer = new Person(); //  타입 구조가 같으므로 오류가 발생하지 않는다.

타입스크립트는 구조적 타입 시스템을 가지고 있기 때문에 Person과 Customer은 같은 구조를 갖고 있어 오류가 없다. 따라서 클래스 인스턴스를 다른 클래스의 변수에 할당해도 된다.

class Person {
	name: String;
}

class Customer {
	name: String;
}

const cust: Customer = {name: 'MARY'};
const pers: Person ={name: "himchan"};

위 처럼 객체 리터럴을 사용해 구조가 동일한 객체를 만들어 클래스 타입 변수나 상수에 할당할 수도 있다.

class Person {
	name: string;
}

class Customer {
	name: string;
	age: number;
}

const cust: Customer = new Person(); // 타입이 일치하지 않습니다

에러가 나는 이유는 cust는 age 프로퍼티가 없으므로 Person 객체 내 age 프로퍼티 메모리를 할당할 수 없기 때문이다.

class Person {
	name: string;
	age: number;
}

class Customer {
	name: string;
}

const cust: Customer = new Person(); // 에러 없음

Person과 Customer가 같은 구조를 갖추었음을 확인한다. 앞선 코드에서 name이란 프로퍼티를 가진 Customer 타입의 상수를 사용해 똑같이 name 프로퍼티를 갖는 Person 객체를 가리키려 했다.

커스텀 타입의 유니온

export class SearchAction {
	actionType = 'SEARCH';
	constructor(readonly playload: {searchQuery: string}){}
}

export class SearchSuccessAction {
	actionType = 'SEARCH_SUCCESS';
	constructor(readonly playload: {searchResults: string[]}){}
}

export type SearchActions = SearchAction | SearchSuccessAction

interface Rectangle {
    kind: "rectangle"; 
    width: number;
    height: number;
}
interface Circle {
    kind: "circle"; 
    radius: number;
}

type Shape = Rectangle | Circle;

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

const myRectangle: Rectangle = { kind: "rectangle", width: 10, height: 20 };
console.log(`Rectangle's area is ${area(myRectangle)}`);

const myCircle: Circle = { kind: "circle", radius: 10};
console.log(`Circle's area is ${area(myCircle)}`);

타입가드 in

타입가드 in은 타입 범위를 축하는 표현이다.

interface A {a: number};
interface B {b: string};

function foo(x: A|B){
if ('a' in x){
		return x.a;
	}

return x.b;
}

any, known

any 타입의 변수는 모든 타입의 값을 가질 수 있다. 타입스크립트에서 타입을 작성하지 않는다면 any와 다름없다. 즉, any를 쓰는 것은 지양해야 한다.

unknown 타입은 타입스크립티 3.0에서 도입되었다. 컴파일러는 프로퍼티에 접근하기 전, unknwon 타입의 변수에 타입 범위를 줄이라고 경고한다. 따라서 런타임 오류를 방지할 수 있다.

any

type Person {
	address: string;
}

let person1:any; // any 타입의 변수 선언

person1 = JSON.parse({"adress": "부산시 사상구"}); // JSON 문자열을 파싱

console.log(person1.address); // undefined

마지막 줄은 JSON 내 문자열 내 address 오류가 있어서 undefined가 출력된다.

unknown

let person2: unknown;

person2 = JSON.parse({"adress":"부산시 사상구"});

console.log(person2.address); // 컴파일 오류

image

타입가드 1)

const isPerson = (object:any):object is Person => "address" in object;

image

parameterType is Type = true / false 타입스크립트의 타입 추론 기능이다.

isPerson 타입가드를 적용한 구문

if(isPerson(person2)) {
	console.log(person2.address);
} else {
	console.log('person2 is not a Person');
}

그러나, 처음에 만든 타입가드는 isPerson()에 null 값이 들어간다면 typescript → javascript로 컴파일은 되지만 런타임에서 null 값에는 in 키워드를 사용할 수 없는 에러가 발생한다.

타입가드 2)

const isPerson = (object: any): object is Person =>
    !!object && "address" in object;

타입가드 3)

위 코드에서 address 타입의 유무만으로 Person 타입을 식별할 수 있다. 그러나 경우에 따라 한가지 프로퍼티 만으로 타입을 식별하기 어려울 수 있다. Organization, Pet, 등 몇가지 프로퍼티를 더 갖고 있을 수 있다.

type Person {
	discriminator: 'person';
	address: string;
}

const isPerson= (object: any): object is Person => !!object && object.discriminator === 'person'

class Dog {  
   constructor(readonly name: string) { };

   sayHello(): string {
     return 'Dog says hello!';
   }
}

class Fish {  
    constructor(readonly name: string) { };

    dive(howDeep: number): string {
     return `Diving ${howDeep} feet`;
   }
}

class Frog {  
    constructor(readonly name: string) { };
}

type Pet = Dog | Fish | Frog;  

function talkToPet(pet: Pet): string {
  
  if (pet instanceof Dog) {  
    return pet.sayHello();
  } else if (pet instanceof Fish) {
    return 'Fish cannot talk, sorry.';
  } 
  else {
    // A hack to make sure that all union members are processed
    // Try adding the Frog as a member of the union in line 21 
    // and you'll see an error in the line 36 stating that 
    // Frog is not assignable to never. Add another else if
    // and the error will go away
    const ifOtherAnimalBecomesPet: never = pet;
    return ifOtherAnimalBecomesPet;
  }
}

const myDog = new Dog('Sammy');    
const myFish = new Fish('Marry');  

console.log(talkToPet(myDog));  
console.log(talkToPet(myFish));

Summary

  • 개발자들에게 변수 타입 선언을 강제하면 해야 할 일이 더 많아져도 장기적으로 봤을 때 개발 생산성이 눈에 띄께 높아질 것이다.
  • 타입스크립트는 기본 타입도 있고, 커스텀 타입도 있다.
  • 이미 선언된 타입들로 유니온 타입도 가능하다.
  • type, interface, class 연산자를 사용해 새로운 타입을 선언할 수 있다.
  • 타입스크립트는 구조적 타입 시스템을 사용한다.