TypeScript 의 다양한 타이핑 방법

https://slides.com/woongjae/lgcns-ts-2

Lead Software Engineer @ProtoPie

Microsoft MVP

TypeScript Korea User Group Organizer

Marktube (Youtube)

Mark Lee

이 웅재

1. var, let, const 와 type

2. Type System

3. Interface Types

4. Type Alias

5. Interface 와 Type Alias

6. Indexable Types

7. Union Types

8. Type Guard

1. var, let, const 와 type

var 는 문제아

  1. 헷갈리는 함수 레벨 스코프

  2. 중복 선언이 가능

  3. 생략도 가능

  4. 호이스팅

// var.js

// 1. 헷갈리는 함수 레벨 스코프

(function() {
  if (true) {   
    var variable = 'function scope';
  }
  
  console.log(variable);
})();
// var.js

// 2. 중복 선언이 가능

(function() {
  var variable = 'function scope';
  var variable = 'duplicated';
  
  console.log(variable);
})();
// var.js

// 3. 생략도 가능

(function() {
  variable = 'no var';
  
  console.log(variable);
})();

console.log(variable);
// var.js

// 4. 호이스팅

(function() {
  console.log(variable);
  
  var variable = 'hoisted';
})();

(function() {
  var variable;
  
  console.log(variable);
  
  variable = 'hoisted';
})();

let 은 해결사

  1. 블록 레벨 스코프

  2. 중복 선언 => syntax error

  3. 호이스팅 => syntax error

// let.js

// 1. 블록 레벨 스코프
{
  let variable = 'block scope';

  console.log(variable);
}

// 2. 중복 선언 => SyntaxError
{
  let variable = 'block scope';
  let variable = 'duplicated';

  console.log(variable);
}

// 3. 호이스팅 => ReferenceError
{
  console.log(variable);
  let variable = 'hoisted';
}

let 은 변경 가능, const 는 불가능

  • Primitive

  • Reference

// const.js

// Primitive
let a = 'a';
a = 'b';
a;

const c = 'c';
c = 'd'; // TypeError
c;
// const.js

// Reference
let e = {
  foo: 'foo',
};
e = {
  bar: 'bar',
};
e;

const f = {
  foo: 'foo',
};
// f = {
//   foo: 'bar',
// }; TypeError
f.foo = 'bar';
f;

2. Type System

타입 시스템

 

  • 컴파일러에게 사용하는 타입을 명시적으로 지정하는 시스템

  • 컴파일러가 자동으로 타입을 추론하는 시스템

타입스크립트의 타입 시스템

 

  • 타입을 명시적으로 지정할 수 있다.

  • 타입을 명시적으로 지정하지 않으면, 타입스크립트 컴파일러가 자동으로 타입을 추론

형태를 정해둔 함수

자신의 코드에서 해당 함수를 사용하는 사용자

해당 함수를 구현하는 구현자

타입이란 해당 변수가 할 수 있는 일을 결정합니다.

// JavaScript

// f1 이라는 함수의 body 에서는 a 를 사용할 것 입니다.
// a 가 할 수 있는 일은 a 의 타입이 결정합니다.

function f1(a) {
  return a;
}

함수 사용법에 대한 오해를 야기하는 자바스크립트

// JavaScript

// (f2 실행의 결과가 NaN 을 의도한 것이 아니라면)
// 이 함수의 작성자는 매개변수 a 가 number 타입이라는 가정으로 함수를 작성했습니다.

function f2(a) {
  return a * 38;
}

// 사용자는 사용법을 숙지하지 않은 채, 문자열을 사용하여 함수를 실행했습니다.

console.log(f2(10)); // 380
console.log(f2('Mark')); // NaN

타입스크립트의 추론에 의지하는 경우

// 타입스크립트 코드지만,
// a 의 타입을 명시적으로 지정하지 않은 경우이가 때문에 a 는 any 로 추론됩니다.
// 함수의 리턴 타입은 number 로 추론됩니다. (NaN 도 number 의 하나입니다.)

function f3(a) {
  return a * 38;
}

// 사용자는 a 가 any 이기 때문에, 사용법에 맞게 문자열을 사용하여 함수를 실행했습니다.

console.log(f3(10)); // 380
console.log(f3('Mark') + 5); // NaN

noImplicitAny 옵션을 켜면


타입을 명시적으로 지정하지 않은 경우,

타입스크립트가 추론 중 `any` 라고 판단하게 되면,

컴파일 에러를 발생시켜

명시적으로 지정하도록 유도한다.

noImplicitAny 에 의한 방어

// error TS7006: Parameter 'a' implicitly has an 'any' type.

function f3(a) {
  return a * 38;
}

// 사용자의 코드를 실행할 수 없습니다. 컴파일이 정상적으로 마무리 될 수 있도록 수정해야 합니다.

console.log(f3(10));
console.log(f3('Mark') + 5);

number 타입으로 추론된 리턴 타입

// 매개변수의 타입은 명시적으로 지정했습니다.
// 명시적으로 지정하지 않은 함수의 리턴 타입은 number 로 추론됩니다.

function f4(a: number) {
  if (a > 0) {
    return a * 38;
  }
}

// 사용자는 사용법에 맞게 숫자형을 사용하여 함수를 실행했습니다.
// 해당 함수의 리턴 타입은 number 이기 때문에, 타입에 따르면 이어진 연산을 바로 할 수 있습니다.
// 하지만 실제 undefined + 5 가 실행되어 NaN 이 출력됩니다.

console.log(f4(5)); // 190
console.log(f4(-5) + 5); // NaN

strictNullChecks 옵션을 켜면


모든 타입에 자동으로 포함되어 있는

`null` 과 `undefined` 를

제거해줍니다.

number | undefined 타입으로 추론된 리턴 타입

// 매개변수의 타입은 명시적으로 지정했습니다.
// 명시적으로 지정하지 않은 함수의 리턴 타입은 number | undefined 로 추론됩니다.

function f4(a: number) {
  if (a > 0) {
    return a * 38;
  }
}

// 사용자는 사용법에 맞게 숫자형을 사용하여 함수를 실행했습니다.
// 해당 함수의 리턴 타입은 number | undefined 이기 때문에,
// 타입에 따르면 이어진 연산을 바로 할 수 없습니다.
// 컴파일 에러를 고쳐야하기 하기 때문에 사용자와 작성자가 의논을 해야합니다.

console.log(f4(5));
console.log(f4(-5) + 5); // error TS2532: Object is possibly 'undefined'.

명시적으로 리턴 타입을 지정해야할까?

// 매개변수의 타입과 함수의 리턴 타입을 명시적으로 지정했습니다.
// 실제 함수 구현부의 리턴 타입과 명시적으로 지정한 타입이 일치하지 않아 컴파일 에러가 발생합니다.

// error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
function f5(a: number): number {
  if (a > 0) {
    return a * 38;
  }
}

noImplicitReturns 옵션을 켜면


함수 내에서 모든 코드가 값을 리턴하지 않으면,

컴파일 에러를 발생시킨다.

모든 코드에서 리턴을 직접해야한다.

// if 가 아닌 경우 return 을 직접 하지 않고 코드가 종료된다.

// error TS7030: Not all code paths return a value.
function f5(a: number) {
  if (a > 0) {
    return a * 38;
  }
}

매개변수에 object 가 들어오는 경우

// JavaScript

function f6(a) {
  return `이름은 ${a.name} 이고, 연령대는 ${
    Math.floor(a.age / 10) * 10
  }대 입니다.`;
}

console.log(f6({ name: 'Mark', age: 38 })); // 이름은 Mark 이고, 연령대는 30대 입니다.
console.log(f6('Mark')); // 이름은 undefined 이고, 연령대는 NaN대 입니다.

object literal type

function f7(a: { name: string; age: number }): string {
  return `이름은 ${a.name} 이고, 연령대는 ${
    Math.floor(a.age / 10) * 10
  }대 입니다.`;
}

console.log(f7({ name: 'Mark', age: 38 })); // 이름은 Mark 이고, 연령대는 30대 입니다.
console.log(f7('Mark')); // error TS2345: Argument of type 'string' is not assignable to parameter of type '{ name: string; age: number; }'.

나만의 타입을 만드는 방법

interface PersonInterface {
  name: string;
  age: number;
}

type PersonTypeAlias = {
  name: string;
  age: number;
};

function f8(a: PersonInterface): string {
  return `이름은 ${a.name} 이고, 연령대는 ${
    Math.floor(a.age / 10) * 10
  }대 입니다.`;
}

console.log(f8({ name: 'Mark', age: 38 })); // 이름은 Mark 이고, 연령대는 30대 입니다.
console.log(f8('Mark')); // error TS2345: Argument of type 'string' is not assignable to parameter of type 'PersonInterface'.

3. Interface Type

interface - basic

function hello(person: { name: string; age: number; }): void {
    console.log(`안녕하세요! ${person.name} 입니다.`);
}

const p: { name: string; age: number; } = {
    name: 'Mark',
    age: 35
};

hello(p); // 안녕하세요! Mark 입니다.

///////////////////////////////////////////////////////////////

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

function hello(person: Person): void {
    console.log(`안녕하세요! ${person.name} 입니다.`);
}

const p: Person = {
    name: 'Mark',
    age: 35
};

hello(p); // 안녕하세요! Mark 입니다.

interface - optional property (1)

interface Person {
    name: string;
    age?: number;
}

function hello(person: Person): void {
    console.log(`안녕하세요! ${person.name} 입니다.`);
}

const p1: Person = {
    name: 'Mark',
    age: 35
};

const p2: Person = {
    name: 'Anna'
};

hello(p1); // 안녕하세요! Mark 입니다.
hello(p2); // 안녕하세요! Anna 입니다.

interface - optional property (2)

interface Person {
    name: string;
    age?: number;
    [props: string]: any;
}

function hello(person: Person): void {
    console.log(`안녕하세요! ${person.name} 입니다.`);
}

const p1: Person = {
    name: 'Mark',
    age: 35,
};

const p2: Person = {
    name: 'Anna',
    systers: [
        'Sung',
        'Chan'
    ]
};

const p3: Person = {
    name: 'Bokdaengi',
    father: p1,
    mother: p2
};

hello(p1); // 안녕하세요! Mark 입니다.
hello(p2); // 안녕하세요! Anna 입니다.
hello(p3); // 안녕하세요! Bokdaengi 입니다.

interface - function in interface

interface Person {
    name: string;
    age: number;
    hello(): void;
}

const p1: Person = {
    name: 'Mark',
    age: 35,
    hello: function (): void {
        console.log(this);
        console.log(`안녕하세요! ${this.name} 입니다.`);
    }
};

const p2: Person = {
    name: 'Mark',
    age: 35,
    hello(): void {
        console.log(this);
        console.log(`안녕하세요! ${this.name} 입니다.`);
    }
};

const p3: Person = {
    name: 'Mark',
    age: 35,
    hello: (): void => {
        console.log(this);
        console.log(`안녕하세요! ${this.name} 입니다.`);
    }
};

p1.hello(); // 안녕하세요! Mark 입니다.
p2.hello(); // 안녕하세요! Mark 입니다.
p3.hello(); // 안녕하세요! 입니다.

class implements interface

interface IPerson {
    name: string;
    age?: number;
    hello(): void;
}

class Person implements IPerson {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    hello(): void {
        console.log(`안녕하세요! ${this.name} 입니다.`);
    }
}

const person = new Person('Mark');
person.hello(); // 안녕하세요! Mark 입니다.

interface extends interface

interface Person {
    name: string;
    age?: number;
}

interface Korean extends Person {
    city: string;
}

const k: Korean = {
    name: '이웅재',
    city: '서울'
};

function interface

interface HelloPerson {
    // (name: string, age: number): void;
    (name: string, age?: number): void;
}

let helloPerson: HelloPerson = function (name: string) {
    console.log(`안녕하세요! ${name} 입니다.`);
};

helloPerson('Mark'); // 안녕하세요! Mark 입니다.

/*

함수의 타입 체크는 할당할때가 아니라 사용할때 한다는 점을 명심

*/

4. Type Alias

타입 별칭 (별명)

  • 인터페이스랑 비슷해 보입니다.

  • Primitive, Union Type, Tuple

  • 기타 직접 작성해야하는 타입을 다른 이름을 지정할 수 있습니다.

  • 만들어진 타입의 refer 로 사용하는 것이지 타입을 만드는것은 아닙니다.

Aliasing Primitive

type MyStringType = string;

const str = 'world';

let myStr: MyStringType = 'hello';
myStr = str;

/*

별 의미가 없다..

*/

Aliasing Union Type

let person: string | number = 0;
person = 'Mark';

type StringOrNumber = string | number;

let another: StringOrNumber = 0;
another = 'Anna';

/*

1. 유니온 타입은 A 도 가능하고 B 도 가능한 타입
2. 길게 쓰는걸 짧게

*/

Aliasing Tuple

let person: [string, number] = ['Mark', 35];

type PersonTuple = [string, number];

let another: PersonTuple = ['Anna', 24];

/*

1. 튜플 타입에 별칭을 줘서 여러군데서 사용할 수 있게 한다.

*/

5. Interface 와 Type Alias

structural type system - 구조가 같으면, 같은 타입이다.

interface IPerson {
  name: string;
  age: number;
  speak(): string;
}

type PersonType = {
  name: string;
  age: number;
  speak(): string;
};

let personInterface: IPerson = {} as any;
let personType: PersonType = {} as any;

personInterface = personType;
personType = personInterface;

nominal type system - 구조가 같아도 이름이 다르면, 다른 타입이다.

type PersonID = string & { readonly brand: unique symbol };

function PersonID(id: string): PersonID {
  return id as PersonID;
}

function getPersonById(id: PersonID) {}

getPersonById(PersonID('id-aaaaaa'));
getPersonById('id-aaaaaa'); // error TS2345: Argument of type 'string' is not assignable to parameter of type 'PersonID'. Type 'string' is not assignable to type '{ readonly brand: unique symbol; }'.

duck typing
만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면
나는 그 새를 오리라고 부를 것이다.

class Duck:
   def sound(self):
      print u"꽥꽥"

class Dog:
   def sound(self):
      print u"멍멍"

def get_sound(animal):
   animal.sound()

def main():
   bird = Duck()
   dog = Dog()
   get_sound(bird)
   get_sound(dog)

function

// type alias
type EatType = (food: string) => void;

// interface
interface IEat {
  (food: string): void;
}

array

// type alias
type PersonList = string[];

// interface
interface IPersonList {
  [index: number]: string;
}

intersection

interface ErrorHandling {
  success: boolean;
  error?: { message: string };
}

interface ArtistsData {
  artists: { name: string }[];
}

// type alias
type ArtistsResponseType = ArtistsData & ErrorHandling;

// interface
interface IArtistsResponse extends ArtistsData, ErrorHandling {}

let art: ArtistsResponseType;
let iar: IArtistsResponse;

union types

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

type PetType = Bird | Fish;

interface IPet extends PetType {} // error TS2312: An interface can only extend an object type or intersection of object types with statically known members.

class Pet implements PetType {} // error TS2422: A class can only implement an object type or intersection of object types with statically known members.

Declaration Merging - interface

interface MergingInterface {
  a: string;
}

interface MergingInterface {
  b: string;
}

let mi: MergingInterface;
mi.

Declaration Merging - type alias

type MergingType = {
  a: string;
};

type MergingType = {
  b: string;
};

[Quiz]

언제 type alias 를 사용하고,

언제 interface 를 사용해야 할까요??

6. Indexable Types

string OR number

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

const sa: StringArray = {}; // 옵셔널하다
sa[100] = '백';

interface StringDictionary {
    [index: string]: string;
}

const sd: StringDictionary = {}; // 옵셔널하다
sd.hundred = '백';

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

const sad: StringArrayDictionary = {};
// 당연히 옵셔널하다.
sad[100] = '백';
sad.hundred = '백';

string index = optional property

interface StringDictionary {
    [index: string]: string;
    name: string;
}

const sd: StringDictionary = {
    name: '이름' // 필수
};

sd.any = 'any'; // 어떤 프로퍼티도 가능

////////////////////////////////////////////////

interface StringDictionaryNo {
    [index: string]: string;
    // name: number; // (X) 인덱서블 타입이 string 값을 가지기 때문에 number 를 필수로 끌어오면 에러
}

7. Union Types

Result1 은 optional type

r1 의 data 가 있으면, error 는 null 이고 loading 은 false

type Result1<T> = {
  data?: T;
  error?: Error;
  loading: boolean;
};

declare function getResult1(): Result1<string>;

const r1 = getResult1();
r1.data; // string | undefined
r1.error; // Error | undefined
r1.loading; // boolean

if (r1.data) {
  r1.error; // Error | undefined
  r1.loading; // boolean
}

Result2 는 union type

`in` operator type guard 를 활용하여, r2 를 제한시켜 처리

type Result2<T> =
  | { loading: true }
  | { data: T; loading: false }
  | { error: Error; loading: false };

declare function getResult2(): Result2<string>;

const r2 = getResult2();

r2.data; // error! Property 'data' does not exist on type 'Result2<string>'. Property 'data' does not exist on type '{ loading: true; }'.
r2.error; // error! Property 'error' does not exist on type 'Result2<string>'. Property 'error' does not exist on type '{ loading: true; }'.
r2.loading; // boolean

if ('data' in r2) {
  r2.error; // error! Property 'error' does not exist on type '{ data: string; loading: false; }'.
  r2.loading; // false
}

Result3 는 union type

type guard 를 활용하여, r3 를 명시적으로 제한시켜 처리

type Result3<T> =
  | { type: 'pending'; loading: true }
  | { type: 'success'; data: T; loading: false }
  | { type: 'fail'; error: Error; loading: false };

declare function getResult3(): Result3<string>;

const r3 = getResult3();

if (r3.type === 'success') {
  r3; // { type: 'success'; data: string; loading: false; }
}
if (r3.type === 'pending') {
  r3; // { type: 'pending'; loading: true; }
}
if (r3.type === 'fail') {
  r3; // { type: 'fail'; error: Error; loading: false; }
}

Union Type 과 Literal Type Guard

interface Dog {
  kind: 'dog';
  eat: () => string;
}

interface Cat {
  kind: 'cat';
  jump: () => string;
}

interface Cow {
  kind: 'cow';
  milk: () => string;
}

type Pet = Dog | Cat | Cow;

function stringifyPaymentMethod(pet: Pet): string {
  switch (pet.kind) {
    case 'dog':
      return pet.eat();
    case 'cat':
      return pet.jump();
    case 'cow':
      return pet.milk();
  }
}

8. Type Guard

1. typeof Type Guard - 보통 Primitive 타입일 경우

function getNumber(value: number | string): number {
  value; // number | string
  if (typeof value === 'number') {
    value; // number
	return value;
  }
  value; // string
  return -1;
}

2. instanceof Type Guard

interface IMachine {
  name: string;
}

class Car implements IMachine {
  name: string;
  wheel: number;
}

class Boat implements IMachine {
  name: string;
  motor: number;
}

function getWhellOrMotor(machine: Car | Boat): number {
  if (machine instanceof Car) {
    return machine.wheel; // Car
  } else {
    return machine.motor; // Boat
  }
}

2. instanceof Type Guard - Error 객체 구분에 많이 쓰인다.

class NegativeNumberError extends Error {}

function getNumber(value: number): number | NegativeNumberError {
  if (value < 0) return new NegativeNumberError();

  return value;
}

function main() {
  const num = getNumber(-10);

  if (num instanceof NegativeNumberError) {
    return;
  }

  num; // number
}

3. in operator Type Guard - object 의 프로퍼티 유무로 처리하는 경우

interface Admin {
  id: string;
  role: string:
}

interface User {
  id: string;
  email: string;
}

function redirect(user: Admin | User) {
  if(/*user is admin*/) {
    routeToAdminPage(usr.role);
  } else {
    routeToHomePage(usr.email);
  }
}

3. in operator Type Guard - object 의 프로퍼티 유무로 처리하는 경우

interface Admin {
  id: string;
  role: string:
}

interface User {
  id: string;
  email: string;
}

function redirect(user: Admin | User) {
  if("role" in user) {
    routeToAdminPage(user.role);
  } else {
    routeToHomePage(user.email);
  }
}

4. literal Type Guard - object 의 프로퍼티가 같고, 타입이 다른 경우

interface IMachine {
  type: string;
}

class Car implements IMachine {
  type: 'CAR';
  wheel: number;
}

class Boat implements IMachine {
  type: 'BOAT';
  motor: number;
}

function getWhellOrMotor(machine: Car | Boat): number {
  if (machine.type === 'CAR') {
    return machine.wheel;
  } else {
    return machine.motor;
  }
}

5. custom Type Guard

function getWhellOrMotor(machine: any): number {
  if (isCar(machine)) {
    return machine.wheel;
  } else if (isBoat(machine)) {
    return machine.motor;
  } else {
    return -1;
  }
}

function isCar(arg: any): arg is Car {
    return arg.type === 'CAR';
}

function isBoat(arg: any): arg is Boat {
    return arg.type === 'BOAT';
}