본문 바로가기

Study/TypeScript

Advanced Types

intersection types

// 1. object 
type ErrorHandling = {
    success: boolean;
    error?: { message: string };
}

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

type ArtworksResponse = ArtworksData & ErrorHandling
// interface ArtworksResponse extends ArtworksData, ErrorHandling {};

const test: ArtworksResponse = {
    artworks: [{ title: 'test' }],
    success: true,
}

// 2. etc

type Languages = 'english' | 'spanish' | 'latin' | 'javascript'
type CodeCamps = 'data structure' | 'algorithm' | 'javascript'
type Courses = Languages & CodeCamps

/*
type Couses = "javascript" 
*/

 

  1. 객체 타입 | 두 객체 타입이 각각 가지고 있는 속성들을 하나로 결합해서, 새로운 타입을 만들 때 사용한다.
  2. 객체 외 타입 | 객체 타입 뿐만 아니라 다른 모든 타입에서도 &를 활용할 수 있는데, 이 때에는 두 타입이 공통적으로 가지고 있는(교차하는, intersection) 타입을 새로운 타입으로 만든다. 

type guards with union

유니언 타입은 타입에 유연성을 부여해 주지만, 코드의 실행 시(런타임) 정확히 어떤 타입을 가져야 하는지를 알아야만 할 때도 있다. 타입 가드는 이런 때에 유니언 타입을 돕는 역할을 한다. 

1. typeof opreator

type Combinable = string | number;

function add(x: Combinable, y: Combinable) {
    // compile error! 
    // Operator '+' cannot be applied to types 'Combinable' and 'Combinable'.
    return x + y;
}

function add(x: Combinable, y: Combinable) {
    // typeof를 이용한 type guards
    if(typeof x === 'string' || typeof y === 'string') {
        return x.toString() + y.toString();
    } 
    return x + y;
}

 

  1. 위와 같이 typeof 연산자를 이용해 타입 가드를 작성할 수 있다.
  2. 그러나 사용자가 정의한 객체 타입을 typeof의 피연산자로 전달하면 object가 반환되기 때문에(js는 사용자 정의 타입을 인식하지 못한다), 이런 경우에 typeof를 타입 가드로 이용하기 어렵다. => in operator 

2. in operator

type Employee = {
    name: string;
    date: Date; 
}
type Admin = {
    name: string;
    privileges: string[]; 
}
type Unknown = Employee | Admin;

// 1.
function getInformation(employee: Unknown) {
    console.log('name', employee.name); 
    // compile error!
    console.log('privileges', employee.privileges); 
}
/* 
Property 'privileges' does not exist on type 'Unknown'.
Property 'privileges' does not exist on type 'Employee' 
*/

// 2. 
function getInformation(employee: Unknown) {
    console.log('name', employee.name); 
    if("privileges" in employee) {
      console.log('privileges', employee.privileges);
    }
}

 

  1. Unknown 타입에는 privileges 속성이 존재하지 않을 수 있기 때문에, 컴파일 에러가 발생한다. 
  2. 이 때 in operator를 사용해서 피연산자 객체 내에 특정 속성이 존재하지 않는지를 체크하면, 컴파일 에러를 제거할 수 있다. 

3. instanceof opreator

어떤 객체가 특정 클래스에 의해 생성되었는지 체크하는 instanceof 연산자를 이용해서, 타입 가드를 실행할 수도 있다. interface는 instanceof의 피연산자가 될 수 없다. 

 

class Car {
    drive() {
        console.log('driving')
    }
}
class Truck {
    drive() {
        console.log('driving')
    }
    load(amount: number) {
        console.log('loaded' + amount)
    }
}

type Vehicle = Car | Truck; 

// 1. 
function getVeihicle(vehicle: Vehicle){
    if('load' in vehicle) {
      console.log(vehicle.load(1000));
    }    
}

// 2. 
function getVeihicle(vehicle: Vehicle){
    if(vehicle instanceof Truck) {
      console.log(vehicle.load(1000));
    }    
}

 

  1. 첫번째 getVehicle 함수와 같이 처리할 수도 있지만, load를 타이핑할 때 오타가 발생하면 안된다는 단점이 있다. 
  2. instanceof 연산자를 이용하면, 위와 같은 문제 없이 처리할 수 있다. 

discriminated unions 

discriminated union(구별된 유니언)은 유니언 타입의 타입 가드를 더욱 간편하게 작성할 수 있도록 도와준다. 

 

interface Bird {
  type: 'bird'; // 이 때 'bird'는 문자열 값이 아니라, 리터럴 타입이 된다
  flyingSpeed: number; 
}
interface Horse {
  type: 'horse'; 
  runningSpeed: number; 
}

type Animal = Bird | Horse; 

function moveAnimal(animal: Animal) {
  // if(flyingSpeed in animal)... 대신 switch를 사용한다 
  let speed; 
  switch (animal.type) {
    case 'bird':  // 자동완성 
      speed = animal.flyingSpeed; 
      break; 
    case 'horse':  
      speed = animal.runningSpeed; 
      break; 
  }
  console.log(speed); 
}

 

  1. interface에서 type 속성을 정의하고(프로퍼티 키가 type일 필요는 없음), 데이터의 타입을 'bird', 'horse'와 같이 리터럴 타입으로 정의한다. 
  2. switch 문에서 type 값을 인자로 받아서 원하는 처리를 할 수 있다. 

type casting(형 변환)

형 변환을 이용하면 타입스크립트가 인식하지 못하는 특정 타입을 명시해 줄 수 있다. 

 

// 1. 
const p = document.querySelector('p');
// const p: HTMLParagraphElement | null

const userNameInput = document.querySelector('#userName');
// const userNameInput: HTMLElement | null

userNameInput.value = 'Harry'; // compile error! 
// => 1) object is possibly null
// => 2) property value does not exists in type HTMLElement

// 2. 
const userNameInput = <HTMLInputElement>document.querySelector('#userName')!;
const userNameInput = document.querySelector('#userName')! as HTMLInputElement;
userNameInput.value = 'Harry'; // no error!

//3. 2번과 동일한 일을 수행한다
const userNameInput = document.querySelector('#userName');

if(userNameInput) {
  (userNameInput as HTMLInputElement).value = 'Harry'; 
}

 

  1. tsconfig.json 파일의 컴파일 옵션 중 libs에 dom이 포함되어 있기 때문에, ts는 HTMLElement 타입을 인식할 수 있다. 
  2. p 태그가 존재하지 않을 경우도 있으므로, HTMLParagraphElement | null로 p의 타입이 추론된다. 
  3. userName의 경우 id 선택자로 요소를 선택하고 있고, 타입은 HTMLElement | null로 추론된다. 즉, 타입스크립트는 userName이 어떤 태그 요소인지 정확히 알지 못한다. 
  4. !를 사용하면, userNameInput이 null로 추론되는 것을 막을 수 있다. 
    • 개발자로서, 어떤 표현식에 의해 생성(반환)되는 값이 null이 아님을 확신할 때 !를 사용할 수 있다. 
    • => 예를 들어, 타입스크립트는 #userInput 요소가 존재하는지 아닌지 확신할 수 없기 때문에 타입 추론에 nul을 포함하지만, 개발자로서 우리는 html에 #userInput 요소가 존재하는 것을 확신할 수 있기 때문에 !를 사용할 수 있다. 
  5. typecasting을 이용하면, HTMLElement의 타입을 보다 구체적인 HTMLInputElement로 지정해서, value 프로퍼티에 접근할 수 있다. typecasting에는 다음 두가지 방법이 있다. 
    • <type> | 타입을 명시하고자 하는 표현식(expression) 앞에 <타입>을 작성한다. 
    • as type | 리액트의 jsx에서 사용되는 사용자 정의 태그 문법과의 충돌을 막기 위해, 표현식 뒤에 as 타입을 작성한다. 

index type

interface ErrorContainer {
  [key: string]: string; 
  id: number; // compile error! 
}

const errorMessage: ErrorContainer = {
  email: 'not a valid email',
  userName: 'must start with a character',
}

 

  1. [key: string] | ErrorContainer 타입 객체의 모든 프로퍼티는 문자열 key와, 문자열 value를 가져야 한다는 것을 의미한다. 
    • 숫자 value를 갖는 id 프로퍼티를 타입 구조에 추가하려고 하면, 컴파일 에러가 발생한다. 
  2. 몇 개의 프로퍼티를 가져야 할지, 프로퍼티의 key는 무엇인지 고정하지 않기 때문에 유연하게 사용할 수 있다. 

function overload

type Combinable = string | number;

function add(x: Combinable, y: Combinable) {
    if(typeof x === 'string' || typeof y === 'string') {
        return x.toString() + y.toString();
    } 
    return x + y;
}

const greet = add('hello', 'world'); 
// 반환값의 type이 string이 아니라 Combinable(number | string)로 추론된다

greet.split(' '); // compile error!

// 1. as 
const greet = add('hello', 'world') as string; 
greet.split(' '); // no error!

// 2. function overloads
function add(x: number, y:number): number; // overload 
function add(x: string, y:string): string;
function add(x: number, y:string): string;
function add(x: string, y:number): string;
function add(x: Combinable, y: Combinable) {
    if(typeof x === 'string' || typeof y === 'string') {
        return x.toString() + y.toString();
    } 
    return x + y;
}

 

  1. add 함수의 실행 결과 값의 타입이 Combinable로 추론되는 것에는 문제가 있다. 
  2. 실제로는 string을 반환하더라도, string 메서드를 사용할 수 없기 때문이다(number의 경우도 마찬가지이다). 
    • as를 이용해 타입을 명시해 줄 수 있다. 
    • 그러나 함수를 호출할 때 마다 모든 반환 값 뒤에 as를 사용해 주어야 한다는 단점이 있다. 
  3. function overload 
    • 함수 선언문 위에 작성할 수 있다. 여기에 매개변수의 타입에 따라 함수가 반환하는 값이 무엇인지 작성한다.
    • ts는 함수 선언문과 overload의 정보를 조합해서 타입을 추론한다.  

nullish coalescing(병합)

// 1. 
const userInput = ''; // 런타임에서 결정된다
const storedData = userInput || 'default';
console.log(stroedData); // default

// 2. 
const storedData = userInput ?? 'default';
console.log(stroedData); // ''

 

  1. || 연산자는 null이나 undefined 뿐만 아니라 비어있는 문자열, 숫자 0도 falsy한 값으로 취급한다. 
    • userInput에 텅 빈 문자열이 저장되었을 때, userInput은 falsy한 값이므로 storedData에 default가 fallback으로 저장된다. 
  2. ?? 연산자(nullish coalescing operator)를 사용하면, null과 undefined 만을 falsy한 값으로 취급할 수 있다. 

'Study > TypeScript' 카테고리의 다른 글

Decorators  (0) 2022.08.03
Generic  (0) 2022.07.24
Interface  (0) 2022.07.21
Class  (0) 2022.07.21
tsconfig | ts compiler  (0) 2022.07.17