본문 바로가기

온라인 강의(유데미, 인프런 등)/한 입 크기로 잘라먹는 타입스크립트(인프런)

[한 입 크기로 잘라먹는 타입스크립트] 함수 타입

반응형

1. 함수타입

자바스크립트 함수를 다른 사람에게 설명하는 가장 좋은 방법은 이 함수가 어떤 매개변수를 받고 어떤 값을 반환 하는지 이야기 하는 것이다. 그럼 타입스크립트에서는 어떤 타입의 매개변수를 받고, 어떤 타입의 값을 반환하는지 이야기 하면 된다.

function func(a: number, b: number): number {
  return a + b;
}

화살표 함수의 타입 정의 방식도 함수 선언식과 동일하다.

const add = (a: number, b: number): number => a + b;

# 파라미터 기본값 설정하기

다음과 같이 함수의 파라미터에 기본값이 설정되어있으면 타입이 자동으로 추론된다. 이럴 경우 타입 정의를 생략해도 된다.

function introduce(name = "이정환") {
	console.log(`name : ${name}`);
}

# 선택적 파라미터 설정하기

다음과 같이 매개변수의 이름뒤에 물음표(?)를 붙여주면 선택적 매개변수가 되어 생략이 가능합니다.

function introduce(name = "이정환", tall?: number) {
  console.log(`name : ${name}`);
  console.log(`tall : ${tall}`);
}

introduce("이정환", 156);

introduce("이정환");

위 코드의 tall 같은 선택적 파라미터 타입은 자동으로 undefined와 유니온 된 타입으로 추론된다. 따라서 tall의 타입은 현재 number | undefined이 된다. 그러므로 이 값이 number 타입의 값일 거라고 기대하고 사용하려면 다음과 같이 타입 좁히기가 필요하다.

function introduce(name = "이정환", tall?: number) {
  console.log(`name : ${name}`);
  if (typeof tall === "number") {
    console.log(`tall : ${tall + 10}`);
  }
}

또 한가지 주의할 점은 선택적 매개변수는 필수 매개변수 앞에 올 수 없고 반드시 뒤에 배치해야 한다.

function introduce(name = "이정환", tall?: number, age: number) {
	// 오류!
  console.log(`name : ${name}`);
  if (typeof tall === "number") {
    console.log(`tall : ${tall + 10}`);
  }
}

# rest 파라미터

function getSum(...rest: number[]) {
  let sum = 0;
  rest.forEach((it) => (sum += it));
  return sum;
}

// 만약 rest 파라미터의 길이를 고정하고 싶다면 다음과 같이 튜플 타입을 이용해도 된다.

function getSum(...rest: [number, number, number]) {
  let sum = 0;
  rest.forEach((it) => (sum += it));
  return sum;
}

getSum(1, 2, 3)    // ✅
getSum(1, 2, 3, 4) // ❌

 

2. 함수 타입 표현식과 호출 시그니쳐

1) 함수 타입 표현식

다음과 같이 함수 타입을 타입 별칭과 함께 별도로 정의할 수 있다. 이를 함수 타입 표현식(Function Type Expression)이라고 부른다. 함수 타입 표현식은 다음과 같이 여러 개의 함수가 동일한 타입을 갖는 경우에 요긴하게 사용된다.

type Operation = (a: number, b: number) => number;

const add: Operation = (a, b) => a + b;
const sub: Operation = (a, b) => a - b;
const multiply: Operation = (a, b) => a * b;
const divide: Operation = (a, b) => a / b;

2) 호출 시그니처

호출 시그니쳐(Call Signature)는 함수 타입 표현식과 동일하게 함수의 타입을 별도로 정의하는 방식이다. 자바스크립트에서는 함수도 객체이기 때문에, 위 코드처럼 객체를 정의하듯 함수의 타입을 별도로 정의할 수 있다.

type Operation2 = {
  (a: number, b: number): number;
};

const add2: Operation2 = (a, b) => a + b;
const sub2: Operation2 = (a, b) => a - b;
const multiply2: Operation2 = (a, b) => a * b;
const divide2: Operation2 = (a, b) => a / b;

참고로 이때 다음과 같이 호출 시그니쳐 아래에 프로퍼티를 추가 정의하는 것도 가능하다. 이렇게 할 경우 함수이자 일반 객체를 의미하는 타입으로 정의되며 이를 하이브리드 타입이라고 부른다.

type Operation2 = {
  (a: number, b: number): number;
  name: string;
};

const add2: Operation2 = (a, b) => a + b;
(...)

add2(1, 2);
add.name;

 

3. 함수 타입의 호환성

함수 타입의 호환성이란 특정 함수 타입을 다른 함수 타입으로 괜찮은지 판단하는 것을 의미한다. 다음 2가지 기준으로 함수 타입의 호환성을 판단하게 된다.

  • 두 함수의 반환값 타입이 호환되는가?
  • 두 함수의 파라미터의 타입이 호환되는가?

# 기준 1 : 반환값 타입이 호환되는가?

A와 B 함수 타입이 있다고 가정할 때 A 반환값 타입이 B 반환값 타입의 슈퍼타입이라면 두 타입은 호환된다. A의 반환값 타입은 Number, B의 반환값 타입은 Number Literal 이다. 따라서 변수 a에 b를 할당하는 것은 가능하나 반대로는 불가능 하다.

type A = () => number;
type B = () => 10;

let a: A = () => 10;
let b: B = () => 10;

a = b; // ✅
b = a; // ❌

# 기준 2 :  파라미터의 타입이 호환되는가?

두번째 기준인 매개변수의 타입이 호환되는지 판단할 때에는 두 함수의 매개변수의 개수가 같은지 다른지에 따라 두가지 유형으로 나뉘게 됩니다.

1) 파라미터의 개수가 같을 때

두 함수 타입 C와 D가 있다고 가정할 때 두 타입의 매개변수의 개수가 같다면 C 매개변수의 타입이 D 매개변수 타입의 서브 타입일 때에 호환된다.

type C = (value: number) => void;
type D = (value: 10) => void;

let c: C = (value) => {};
let d: D = (value) => {};

c = d; // ❌
d = c; // ✅

C 매개변수의 타입은 Number, D 매개변수의 타입은 Number Literal 이다. 따라서 C 매개변수의 타입이 D 매개변수의 슈퍼타입이므로 D를 C로 취급하는것은 불가능하나 반대로는 가능하다.

이는 반환값 타입과 반대된다. 마치 다운캐스팅을 허용하는 것 같아 보인다. 이렇게 되는 이유는 두 함수의 매개변수의 타입이 모두 객체 타입일때 좀 더 이해가 쉽다.

type Animal = {
  name: string;
};

type Dog = {
  name: string;
  color: string;
};

let animalFunc = (animal: Animal) => {
  console.log(animal.name);
};

let dogFunc = (dog: Dog) => {
  console.log(dog.name);
  console.log(dog.color);
};

animalFunc = dogFunc; // ❌
dogFunc = animalFunc; // ✅

animalFunc에 dogFunc를 할당하는 것은 불가능하다. dogFunc의 파라미터 타입이 animalFunc 파라미터 타입보다 작은 서브타입이기 때문이다. 반대로는 가능하다.

animalFunc = dogFunc를 코드로 표현해보면 다음과 같다.

let animalFunc = (animal: Animal) => {
  console.log(animal.name);  // ✅
  console.log(animal.color); // ❌
};

animalFunc 타입의 매개변수 타입은 Animal 타입이다. 그러나 dogFunc 함수 내부에서는 name과 color 프로퍼티에 접근한다. 따라서 이렇게 할당이 이루어지게 되면 animal.color처럼 존재할거라고 보장할 수 없는 프로퍼티에 접근하게 된다.

반대로 dogFunc = animalFunc를 코드로 표현하면 다음과 같다.

let dogFunc = (dog: Dog) => {
  console.log(dog.name);
};

2) 파라미터의 개수가 다를 때

type Func1 = (a: number, b: number) => void;
type Func2 = (a: number) => void;

let func1: Func1 = (a, b) => {};
let func2: Func2 = (a) => {};

func1 = func2; // ✅
func2 = func1; // ❌

 

4. 함수 오버로딩

함수 오버로딩이란 하나의 함수를 매개변수의 개수나 타입에 따라 다르게 동작하도록 만드는 문법이다.

/**
 * 함수 오버로딩
 * 같은 함수를 매개변수의 개수나 타입에 따라
 * 여러가지 버전으로 만드는 문법
 * -> 하나의 함수 func
 * -> 일단 모든 매개변수는 넘버타입
 * -> Ver1. 매개변수가 1개일 때에는 매개변수에 20을 곱한 값을 출력
 * -> Ver2. 매개변수가 3개일 때에는 모든 매개변수를 더한 값을 출력
 */

타입스크립트에서 함수 오버로딩을 구현하려면 먼저 다음과 같이 버전별 오버로드 시그니쳐를 만들어 줘야 한다.

// 버전들 -> 오버로드 시그니쳐
function func(a: number): void;
function func(a: number, b: number, c: number): void;

이렇게 구현부 없이 선언부만 만들어둔 함수를 ‘오버로드 시그니쳐’라고 한다. 위 코드에서는 2개의 오버로드 시그니쳐를 만들었으며 각각 함수의 버전을 의미한다. 위 코드는 func 함수는 매개변수를 1개 받는 버전과 3개 받는 2개의 버전이 있다고 알리는 것 과 같다.

오버로드 시그니쳐를 만들었다면 다음으로는 구현 시그니쳐를 만들어줘야 한다. 구현 시그니쳐는 실제로 함수가 어떻게 실행될 것인지를 정의하는 부분이다.

// 버전들 -> 오버로드 시그니쳐
function func(a: number): void;
function func(a: number, b: number, c: number): void;

// 실제 구현부 -> 구현 시그니쳐
function func(a: number, b?: number, c?: number) {
  if (typeof b === "number" && typeof c === "number") {
    console.log(a + b + c);
  } else {
    console.log(a * 20);
  }
}

func(1);        // ✅ 버전 1 - 오버로드 시그니쳐
func(1, 2);     // ❌ 
func(1, 2, 3);  // ✅ 버전 3 - 오버로드 시그니쳐

구현 시그니쳐의 파라미터 타입은 모든 오버로드 시그니쳐와 호환되도록 만들어야 한다. 따라서 위 코드에서는 파라미터 b와 c를 선택적 파라미터로 만들어 매개변수를 하나만 받는 첫번째 오버로드 시그니쳐와도 호환되도록 만들어 준다.

 

5. 사용자 정의 타입가드

사용자 정의 타입가드란 참 또는 거짓을 반환하는 함수를 이용해 우리 입맛대로 타입 가드를 만들 수 있도록 도와주는 타입스크립트의 문법이다.

type Dog = {
  name: string;
  isBark: boolean;
};

type Cat = {
  name: string;
  isScratch: boolean;
};

type Animal = Dog | Cat;

function warning(animal: Animal) {
  if ("isBark" in animal) {
    console.log(animal.isBark ? "짖습니다" : "안짖어요");
  } else if ("isScratch" in animal) {
    console.log(animal.isScratch ? "할큅니다" : "안할퀴어요");
  }
}

2개의 타입 Dog와 Cat을 정의하고 두 타입의 유니온 타입인 Animal 타입을 정의했다.다음으로 매개변수로 Animal 타입의 값을 받아 동물에 따라 각각 다른 경고를 콘솔에 출력하는 함수 warning을 만들어 주었다. 그런데 이렇게 in 연산자를 이용해 타입을 좁히는 방식은 좋지 않다고 이전에 살펴본 적 있다.

예를 들어 만약 Dog 타입의 프로퍼티가 다음과 같이 중간에 이름이 수정되거나 추가 또는 삭제될 경우에는 타입 가드가 제대로 동작하지 않을 수도 있다.

이럴 때에는 다음과 같이 함수를 이용해 커스텀 타입 가드를 만들어 타입을 좁히는게 더 좋다.

(...)

// Dog 타입인지 확인하는 타입 가드
function isDog(animal: Animal): animal is Dog {
  return (animal as Dog).isBark !== undefined;
}

// Cat 타입인지 확인하는 타입가드
function isCat(animal: Animal): animal is Cat {
  return (animal as Cat).isScratch !== undefined;
}

function warning(animal: Animal) {
  if (isDog(animal)) {
    console.log(animal.isBark ? "짖습니다" : "안짖어요");
  } else {
    console.log(animal.isScratch ? "할큅니다" : "안할퀴어요");
  }
}

isDog 함수는 파라미터로 받은 값이 Dog 타입이라면 true 아니라면 false를 반환한다. 이때 반환값의 타입으로 animal is Dog 를 정의하면 이 함수가 true를 반환하면 조건문 내부에서는 이 값이 Dog 타입임을 보장한다는 의미가 된다. 따라서 warning 함수에서 isDog 함수를 호출해 파라미터의 값이 Dog 타입인지 확인하고 타입을 좁힐 수 있다.

 

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8/dashboard

 

한 입 크기로 잘라먹는 타입스크립트 - 인프런 | 강의

문법을 넘어 동작 원리와 개념 이해까지 배워도 배워도 헷갈리는 타입스크립트 이제 제대로 배워보세요! 여러분을 타입스크립트 마법사🧙🏻‍♀️로 만들어드립니다., - 강의 소개 | 인프런

www.inflearn.com

 

반응형