TypeScript / / 2024. 10. 25. 07:39

TypeScript 정리3 (함수와 타입)

함수의 타입 정의

  1. 함수 타입 정의: 함수의 매개변수와 반환값에 타입을 지정하여 함수의 동작을 명확히 설명할 수 있습니다.
  2. function add(a: number, b: number): number { return a + b; }
  3. 화살표 함수: 화살표 함수도 매개변수와 반환값에 타입을 지정할 수 있습니다.
  4. const add = (a: number, b: number): number => a + b;
  5. 선택적 매개변수: 매개변수 뒤에 **?**를 붙이면 선택적으로 값을 받을 수 있으며, 해당 매개변수는 **undefined**와 유니온 타입으로 추론됩니다.
  6. function introduce(name = "이정환", tall?: number) { console.log(`name: ${name}`); if (typeof tall === "number") { console.log(`tall: ${tall}`); } }
  7. 나머지 매개변수(Rest Parameter): 여러 개의 값을 배열로 받아 처리할 수 있으며, 나머지 매개변수는 배열 타입으로 정의됩니다.
  8. function getSum(...numbers: number[]): number { return numbers.reduce((sum, num) => sum + num, 0); }
  9. 튜플 타입의 나머지 매개변수: 나머지 매개변수의 길이를 고정하고 싶을 때는 튜플 타입을 사용합니다.
  10. function getSum(...numbers: [number, number, number]): number { return numbers.reduce((sum, num) => sum + num, 0); }

통합 예시 코드:

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

function getSum(...numbers: number[]): number {
  return numbers.reduce((sum, num) => sum + num, 0);
}

introduce("홍길동", 180);
console.log(getSum(1, 2, 3));  // 6

이 요약은 함수 정의 방식과 선택적 매개변수나머지 매개변수에 대한 설명을 포함한 기본적인 함수 타입 정의 방법을 보여줍니다.


함수 타입 표현식(Function Type Expression)

함수 타입 표현식은 함수의 타입을 별도로 정의하고, 이를 변수나 함수에 적용하는 방식입니다. 타입 별칭을 사용해 함수의 타입을 정의할 수 있으며, 이렇게 하면 함수 선언과 타입 선언을 분리할 수 있어 코드가 더 명확해지고 재사용성이 높아집니다.

예시: 함수 타입 표현식

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

const add: Add = (a, b) => a + b;
  • 여기서 ****Add***는 함수 타입을 표현한 타입 별칭입니다. **a**와 **b**는 number 타입을 받고, number 타입의 값을 반환하는 함수로 정의됩니다.
  • 함수 ****add***는 Add 타입으로 정의된 함수 타입을 따릅니다.

여러 함수에 동일한 타입 적용

함수 타입 표현식을 사용하면 여러 개의 함수에 동일한 타입을 쉽게 적용할 수 있습니다.

예시: 여러 함수에 함수 타입 적용

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;
  • 여기서 Operation 타입은 여러 함수에서 동일하게 사용됩니다. 이렇게 하면 코드의 일관성이 유지되며, 타입 재사용이 용이해집니다.

함수 타입 표현식의 직접 사용

함수 타입 표현식은 타입 별칭 없이도 직접 사용할 수 있습니다.

예시: 함수 타입 표현식의 직접 사용

const add: (a: number, b: number) => number = (a, b) => a + b;
  • 함수 ****add***의 타입을 함수 타입 표현식으로 직접 지정할 수 있습니다.

호출 시그니처(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;
  • Operation2 타입은 객체처럼 함수의 타입을 정의합니다. 함수 add2, sub2 등은 모두 이 타입을 따릅니다.

하이브리드 타입(Hybrid Type)

함수는 객체이기도 하므로, 호출 시그니처와 함께 프로퍼티를 추가하여 함수이자 객체인 타입을 정의할 수 있습니다. 이를 하이브리드 타입이라고 부릅니다.

예시: 하이브리드 타입

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

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

console.log(add2(1, 2));  // 3
console.log(add2.name);   // "Addition"
  • ***Operation2***는 함수 호출 시그니처와 프로퍼티를 동시에 가지고 있어, 함수이자 일반 객체처럼 사용할 수 있습니다.

정리

  • 함수 타입 표현식은 함수의 타입을 매개변수와 반환 타입으로 정의하여 타입 별칭으로 선언할 수 있습니다.
  • 여러 함수에 동일한 타입을 적용할 수 있어 코드 일관성과 유연성을 높여줍니다.
  • *호출 시그니처(Call Signature)**는 객체 형태로 함수의 타입을 정의하며, 이를 통해 함수의 구조를 명확하게 나타낼 수 있습니다.
  • 하이브리드 타입은 함수와 객체의 속성을 동시에 가질 수 있는 타입으로, 함수와 프로퍼티를 함께 사용할 수 있습니다.

통합 예시 코드

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

const add: Operation = (a, b) => a + b;
const sub: Operation = (a, b) => a - b;

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

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

console.log(add(1, 2));  // 3
console.log(sub(5, 2));  // 3
console.log(add2(3, 4)); // 7
console.log(add2.name);  // "Addition"

함수 타입의 호환성

함수 타입의 호환성은 두 함수 타입이 서로 할당 가능한지를 판단하는 것을 의미합니다. 이를 판단하는 기준은 반환값의 타입과 매개변수의 타입입니다. 이를 쉽게 이해하기 위해 두 가지 주요 기준을 살펴보겠습니다.

기준 1: 반환값 타입의 호환성

  • A 함수의 반환값이 B 함수의 반환값의 슈퍼타입일 때, A와 B는 호환됩니다.

예시

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

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

a = b; // ✅ 가능 (B의 반환값이 A의 서브타입)
b = a; // ❌ 불가능 (A의 반환값이 B의 슈퍼타입이므로 할당 불가)
  • ***number***는 ****10***의 슈퍼타입이므로, B는 A로 할당 가능하지만, 반대는 불가능합니다.

첫 번째 추가 예시

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

let a: A = () => 10;  // 반환값이 있지만 무시됨
let b: B = () => 10;

a = b;  // ✅ 가능
b = a;  // ❌ 불가능

왜 a = b는 가능하고 b = a는 불가능할까?

  1. a = b가 가능한 이유:
    • **A**는 () => void, 즉 반환값을 무시하는 함수 타입입니다.
    • **b**는 () => number 타입이지만, 반환값을 무시하는 void 타입에 할당될 때는 반환값이 무시되기 때문에 문제가 없습니다.
    • 즉, **b**는 숫자를 반환하지만, **a**는 그 반환값을 무시하는 함수로 처리되므로 **a = b**는 가능합니다.
  2. b = a가 불가능한 이유:
    • **B**는 반드시 **number**를 반환해야 합니다.
    • 하지만 **a**는 **void**를 반환하는 함수로, 반환값이 없거나 무시되므로 **number**를 기대하는 B 타입에 할당할 수 없습니다.
    • 이 때문에 **b = a**는 불가능합니다.

두 번째 예시

type A = () => unknown;
type B = () => void;

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

a = b;  // ✅ 가능
b = a;  // ✅ 가능

왜 a = b와 b = a 둘 다 가능한가?

  1. a = b가 가능한 이유:
    • **A**는 () => unknown, 즉 모든 타입을 반환할 수 있는 함수입니다.
    • **B**는 () => void, 즉 반환값이 무시되는 함수입니다.
    • **void**는 unknown 타입의 서브타입이기 때문에 **b**를 **a**에 할당할 수 있습니다.
  2. b = a가 가능한 이유:
    • **B**는 반환값이 없어도 되거나 무시될 수 있는 함수입니다.
    • **A**는 unknown 타입을 반환하지만, 이 값이 **void**처럼 처리될 수 있습니다.
    • unknown 타입은 void 타입과 호환 가능하므로, **a**를 **b**에 할당할 수 있습니다.

정리

  1. void 타입은 반환값을 무시하므로, 반환값이 있어도 문제가 되지 않습니다. 그러나 반환값이 요구되는 타입에 반환값이 없는 함수를 할당하는 것은 불가능합니다.
    • 반환값이 있는 함수는 void 타입으로 할당될 수 있지만, 그 반대는 불가능합니다.
  2. unknown 타입은 모든 타입을 반환할 수 있기 때문에, **unknown**과 void는 상호 호환이 가능합니다.

기준 2: 매개변수 타입의 호환성

매개변수 타입의 호환성은 매개변수의 개수에 따라 달라집니다.

2-1: 매개변수의 개수가 같을 때

  • C 함수의 매개변수 타입이 D 함수의 매개변수 타입의 슈퍼타입일 때, D는 C로 호환 가능합니다.

예시

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

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

d = c; // ✅ 가능 (C의 매개변수 타입이 D의 슈퍼타입)
c = d; // ❌ 불가능 (D의 매개변수 타입이 C의 서브타입)
  • ***number***는 ****10***의 슈퍼타입이므로, C는 D에 할당 가능하지만, 반대는 불가능합니다.

객체 타입을 가진 매개변수의 경우

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);
};

dogFunc = animalFunc; // ✅ 가능
animalFunc = dogFunc; // ❌ 불가능
  • ***Dog***는 ****Animal***의 서브타입이므로, ****dogFunc = animalFunc***는 안전하지만 반대로는 불가능합니다. Dog 타입에서 요구하는 color 프로퍼티에 접근하지 않으면 오류가 발생할 수 있기 때문입니다.

2-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; // ❌ 불가능 (매개변수가 더 많음)
  • 매개변수가 더 적은 함수는 매개변수가 더 많은 함수로 안전하게 할당될 수 있습니다. 하지만 반대는 불가능합니다.

요약

  1. 반환값 타입의 호환성: 반환값의 슈퍼타입이 반환값의 서브타입으로 할당 가능.
    • 반환값의 타입이 더 넓은 범위를 포함하는 함수는 더 좁은 범위를 포함하는 함수로 호환 가능.
  2. 매개변수 타입의 호환성:
    • 매개변수 개수가 같을 때매개변수의 슈퍼타입이 서브타입으로 할당 가능.
    • 매개변수 개수가 다를 때: 매개변수가 적은 함수는 매개변수가 더 많은 함수로 할당 가능.

이 두 가지 원칙을 통해 함수 간의 타입 호환성을 이해할 수 있습니다.

추가로

함수의 매개변수와 반환값 타입의 차이

  • 반환값: 함수가 무엇을 반환하는지는 호출하는 사람이 처리해야 합니다. 즉, 반환값이 더 구체적일수록 문제될 가능성이 적습니다. 예를 들어, 더 넓은 범위를 반환해도 호출자는 이를 처리할 수 있기 때문에 문제가 없습니다. 따라서 슈퍼타입이 서브타입으로 업캐스팅될 수 있습니다.
  • 매개변수: 함수에 무엇을 전달하는 것은 호출하는 사람이 해야 합니다. 이 경우 매개변수 타입이 더 구체적이면 함수가 기대하는 것보다 더 많은 정보가 필요하게 되고, 이것이 안전성 문제를 일으킬 수 있습니다. 즉, 함수가 더 구체적인 타입을 기대하는 상황에서 슈퍼타입을 전달하면, 함수 내부에서 접근할 수 없는 프로퍼티에 접근하려고 할 수 있기 때문에 안전하지 않습니다.

왜 매개변수는 다운캐스팅만 허용되는가?

매개변수에서 **다운캐스팅(슈퍼타입 → 서브타입)**이 허용되는 이유는 함수가 기대하는 값의 타입보다 더 넓은 타입을 받는 것이 안전하기 때문입니다. 반면, **업캐스팅(서브타입 → 슈퍼타입)**이 허용되지 않는 이유는 타입 안전성을 보장하기 위함입니다.

예시: Animal과 Dog 타입

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; // ✅ 가능

1. animalFunc = dogFunc (불가능한 이유)

  • ***dogFunc***은 Dog 타입의 매개변수를 요구합니다. 즉, ****name***과 color 프로퍼티가 모두 있어야 합니다.
  • 그러나 ****animalFunc***은 Animal 타입만을 받습니다. ****Dog***가 요구하는 color 프로퍼티가 없기 때문에, ****animalFunc***이 호출되면 ****dogFunc***에서 ****color***에 접근하려다 오류가 발생할 수 있습니다.
  • 즉, 서브타입을 슈퍼타입에 업캐스팅하려 하면 기대되는 프로퍼티를 찾지 못해 안전하지 않습니다.

2. dogFunc = animalFunc (가능한 이유)

  • ***dogFunc***은 Dog 타입의 매개변수를 받습니다. Animal 타입은 Dog 타입의 슈퍼타입이므로, ****dogFunc***이 Animal 타입을 받더라도 문제는 없습니다.
  • 즉, 슈퍼타입을 서브타입에 다운캐스팅하는 것은 안전합니다. Animal 타입의 매개변수를 받더라도 ****dogFunc***이 필요한 프로퍼티(즉, name)만 접근하기 때문에 타입 안전성이 보장됩니다.

핵심 원리: 공변성과 반공변성

이 상황은 **공변성(covariance)**과 **반공변성(contravariance)**의 개념과 관련이 있습니다.

  1. 반환값은 공변적: 반환값은 더 구체적인 서브타입을 반환해도 문제없기 때문에 업캐스팅이 허용됩니다.
  2. 매개변수는 반공변적: 매개변수는 더 구체적인 타입을 받을 수 없으므로 다운캐스팅만 허용됩니다. 이로 인해 함수가 예상하지 못한 타입에 접근하지 않도록 안전성을 보장합니다.

정리

  • 반환값: 반환값은 **더 구체적인 타입(서브타입)**을 반환해도 문제가 없으므로, 업캐스팅이 허용됩니다.
  • 매개변수: 매개변수는 **더 넓은 타입(슈퍼타입)**을 받을 수 있지만, **더 구체적인 타입(서브타입)**을 받으면 예상하지 못한 값에 접근할 수 있으므로 다운캐스팅만 허용됩니다.

함수 오버로딩(Function Overloading)

함수 오버로딩이란 하나의 함수가 매개변수의 개수나 타입에 따라 다르게 동작하도록 만드는 문법입니다. 즉, 같은 이름의 함수지만, 매개변수에 따라 다른 동작을 하도록 여러 가지 버전을 제공하는 것입니다.

타입스크립트에서의 함수 오버로딩 구현

  1. 오버로드 시그니처(Overload Signature)
    • 함수의 각 버전을 정의하는 선언부입니다. 이 선언부에서는 함수의 매개변수 개수나 타입만을 정의하고, 구현부는 작성하지 않습니다.
    • 예시:
    • // 오버로드 시그니처들 function func(a: number): void; function func(a: number, b: number, c: number): void;
  2. 구현 시그니처(Implementation Signature)
    • 실제로 함수가 어떻게 동작하는지를 구현하는 부분입니다.
    • 모든 오버로드 시그니처와 호환되도록 매개변수 설정을 해야 하므로, 선택적 매개변수를 사용하여 구현해야 합니다.
    • 예시:
    • // 오버로드 시그니처들 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); // a에 20을 곱함 } }
  3. 함수 호출 예시
    • 오버로드 시그니처에 맞는 매개변수 개수로 호출하면 정상 작동합니다.
    func(1);        // ✅ 매개변수 1개, a * 20 -> 20
    func(1, 2);     // ❌ 매개변수 2개는 오버로드 시그니처에 없음 (오류 발생)
    func(1, 2, 3);  // ✅ 매개변수 3개, a + b + c -> 6
    

요약

  • 오버로드 시그니처: 함수의 각 버전을 선언하는 부분. 매개변수 개수나 타입을 기준으로 여러 버전의 함수를 선언할 수 있습니다.
  • 구현 시그니처모든 오버로드 시그니처와 호환되는 단일 구현을 작성해야 하며, 선택적 매개변수와 조건문 등을 사용하여 함수의 동작을 제어합니다.

함수 오버로딩을 통해 같은 이름의 함수를 여러 가지 버전으로 사용할 수 있으며, 매개변수에 따라 다른 동작을 수행하도록 구현할 수 있습니다.


사용자 정의 타입 가드 (Custom Type Guard)

사용자 정의 타입 가드는 특정 값이 어떤 타입인지를 커스텀 함수로 판단하여 타입스크립트에서 타입을 안전하게 좁히는 방법입니다. 함수가 참(true**)**을 반환하면 해당 조건 안에서 타입을 좁힐 수 있음을 보장합니다. 이 방법은 in 연산자와 같은 기본적인 타입 가드보다 더 안전하고 유연하게 타입을 좁힐 수 있습니다.

기본 예시

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 ? "할큅니다" : "안할퀴어요");
  }
}
  • 여기서 Animal 타입은 ****Dog***와 Cat 타입의 유니온 타입입니다.
  • warning 함수는 Animal 타입을 인수로 받아 ****Dog***이면 "짖습니다" 또는 "안짖어요"를 출력하고, ****Cat***이면 "할큅니다" 또는 "안 할퀴어요"를 출력합니다.
  • 이때 in 연산자를 사용해 타입을 좁히고 있습니다.

문제점

  • 만약 Dog 타입의 프로퍼티 이름이 변경되면 (isBark → isBarked), 타입 가드가 제대로 동작하지 않을 수 있습니다.

해결 방법: 사용자 정의 타입 가드 사용

사용자 정의 타입 가드를 사용해 타입을 명확하게 구분할 수 있습니다. 이를 통해 프로퍼티 이름이 변경되더라도 안전한 타입 가드를 구현할 수 있습니다.

1. Dog 타입을 확인하는 사용자 정의 타입 가드

function isDog(animal: Animal): animal is Dog {
  return (animal as Dog).isBark !== undefined;
}
  • isDog 함수는 Animal 타입을 인수로 받아 Dog 타입인지를 확인합니다.
  • 반환값의 타입으로 ****animal is Dog***을 지정하면, 이 함수가 **true**를 반환하는 경우 animal이 Dog 타입임을 보장합니다.

2. Cat 타입을 확인하는 사용자 정의 타입 가드

function isCat(animal: Animal): animal is Cat {
  return (animal as Cat).isScratch !== undefined;
}
  • isCat 함수는 Animal 타입을 인수로 받아 Cat 타입인지를 확인합니다.
  • 마찬가지로 반환값의 타입으로 ****animal is Cat***을 지정하여 Cat 타입을 보장합니다.

함수 내부에서 타입 좁히기

이제 사용자 정의 타입 가드를 사용해 warning 함수의 타입을 안전하게 좁힐 수 있습니다.

function warning(animal: Animal) {
  if (isDog(animal)) {
    console.log(animal.isBark ? "짖습니다" : "안짖어요");
  } else if (isCat(animal)) {
    console.log(animal.isScratch ? "할큅니다" : "안할퀴어요");
  }
}
  • ***isDog(animal)***이 참이면, 타입스크립트는 animal이 Dog 타입임을 보장합니다. 따라서 **animal.isBark**에 안전하게 접근할 수 있습니다.
  • ***isCat(animal)***이 참이면, 타입스크립트는 animal이 Cat 타입임을 보장하므로 **animal.isScratch**에 접근할 수 있습니다.

정리

  1. 사용자 정의 타입 가드는 함수의 반환값으로 ****animal is Type***을 사용하여 타입을 명시적으로 좁힐 수 있는 방법입니다.
  2. 이 방법은 기본 타입 가드(in 연산자)보다 유연하고 안전하며, 프로퍼티 변경에도 강한 방식입니다.
  3. 예시:
  4. function isDog(animal: Animal): animal is Dog { return (animal as Dog).isBark !== undefined; } 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 if (isCat(animal)) { console.log(animal.isScratch ? "할큅니다" : "안할퀴어요"); } }

이처럼 사용자 정의 타입 가드를 활용하면 타입스크립트의 타입 시스템을 더욱 안전하게 사용할 수 있습니다.

'TypeScript' 카테고리의 다른 글

자바스크립트, 타입스크립트 스코프,호이스팅  (2) 2024.10.29
TypeScript 인터페이스  (0) 2024.10.29
TypeScript 정리2 (이해하기)  (3) 2024.10.25
TypeScript 정리1  (1) 2024.10.25
TypeScript 기본  (3) 2024.10.25
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유