TS관점에서 함수 더 알아보기
📌 함수 타입 표현식(Function Type Expression)
함수를 설명하는 가장 간단한 방법이다. 이 타입은 화살표 함수와 문법적으로 유사하다.
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
이때, 함수 선언처럼 매개변수 타입이 지정되지 않으면, 암묵적으로 any가 되니 주의!
물론, 타입 별칭을 사용해서 함수의 타입에 이름 붙이기도 가능.
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
📌 호출 시그니처(Call Signatures)
JS에서 함수는 객체이므로, 프로퍼티도 추가할 수 있다. 하지만, 함수 타입 표현식 문법은 프로퍼티를 정의할 수 없음. 이 때 사용할 수 있는 것이 호출 시그니처
매개변수 타입과 반환값의 타입 사이에 =>가 아닌 :를 사용
type Person = {
(a: string): void;
name: string;
};
📌 함수 타입의 호환성
함수가 다른 함수 타입에 할당 가능한지는 생각보다 어렵다.
언젠가 CS에서 공부한 리스코프 치환 원칙 과 구조적 타입 시스템의 반환값 공변성 규칙이 결합되어 동작하기 때문이다.
**리스코프 치환 원칙(LSP)**과 반환값 공변성(Return type covariance)
LSP: 부모 타입이 사용되는 곳에서 자식 타입도 문제없이 동작해야 하는 원칙
Covariance: 함수 반환 타입에서 서브타입을 슈퍼타입 자리에 대체할 수 있는 성질
이걸 좀 쉽게 풀이하면,
- 반환값 타입의 호환성
- 매개변수 타입의 호환성
이 두 가지를 통과해야한다는 뜻이다.
1. 반환값 타입의 호환성
type A = () => number;
type B = () => 2;
let a: A = () => 1;
let b: B = () => 2;
a = b;
b = a; // ❌ Error: 'A' 형식은 'B' 형식에 할당할 수 없습니다.
여기서,
- B = 항상 2만 반환하는 더 좁은 타입(서브타입)
- A = number 전체를 반환할 수 있는 더 넓은 타입(슈퍼타입)
따라서,
✔️ 서브타입을 슈퍼타입으로 치환하는 것(업캐스팅)은 OK,
❌ 슈퍼타입을 서브타입으로 치환하는 것(다운캐스팅)은 타입 안전성 상 위험 → 금지.
2-1. 매개변수 타입의 호환성 : 매개변수의 개수가 같을 때
type C = (value: number) => void;
type D = (value: 10) => void;
let c: C = (value) => {};
let d: D = (value) => {};
c = d; // ❌ Error: 'D' 형식은 'C' 형식에 할당할 수 없습니다.
d = c;
신기하게 이번엔 반대인 것을 볼 수 있다.
여기서,
- D = 항상 2만 받을 수 있는 더 좁은 타입(서브타입)
- C = number 전체를 받을 수 있는 더 넓은 타입(슈퍼타입)
✔️ 슈퍼타입을 서브타입으로 치환하는 것(다운캐스팅)은 OK, ❌ 서브타입을 슈퍼타입으로 치환하는 것(업캐스팅)은 금지.
왜 그런지는 아래 예시가 더 편하게 보여 줄 수 있다.
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; // ❌ Error: '(dog:Dog) => void' 형식은 '(animal:Animal) => void' 형식에 할당할 수 없습니다.
dogFunc = animalFunc;
animalFunc = dogFunc 이 식을 다시 풀어보면,
let animalFunc = (animal: Animal) => {
console.log(animal.name);
console.log(animal.color); // ❌ Error: 'Animal' 형식에 'color' 속성이 없습니다.
};
이런식의 말도 안되는 걸 막기 위해서이다. 반대 상황의 경우에는,
let dogFunc = (dog: Dog) => {
console.log(dog.name);
};
모든 프로퍼티를 dog가 이미 가지고 있기 때문에 문제 없이 돌아간다.
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; // ❌ Error: 'Func1' 형식은 'Func2' 형식에 할당 할 수 없습니다.
이건 의외로 간단하다. 더 많이만 안들어가면 된다. 애초에 JS는 인자를 더 줘도 함수가 알아서 무시하기 때문이다.
📌 함수 오버로딩
함수 오버로딩이란 함수를 매개변수의 개수나 타입에 따라 여러가지 버전으로 나누어 정의하는 방법이다.
JS에서는 없는 TS만의 특별한 문법이다. 하지만, 공식문서에서는 가능하다면 오버로드 대신 유니온 타입을 사용하라고 권장하고 있다.
오버로드 시그니처(overload signatures)와 구현 시그니처(implementation signature)
오버로드 시그니처 : 호출자가 볼 수 있는 함수 사용 설명서(여러 줄)
구현 시그니처 : 실제 구현(오버로드 목록에는 포함 안됨)
// (1) 오버로드 시그니처들: 호출자가 보게 될 타입
function func(a: number): void;
function func(a: number, b: number, c: number): void;
// 구현 시그니처(바디)가 반드시 1개 필요함
// (2) 구현 시그니처: 실제 구현(오버로드 목록에는 포함 안 됨)
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(); // ❌ Error: 1-3개의 인수가 필요한데 0개를 가져왔습니다.
func(1);
func(1, 2); // ❌ Error: 오버로드에 2 인수가 필요하지 않지만, 1 또는 3 인수가 필요한 오버로드가 있습니다.
func(1, 2, 3);
📌 사용자 정의 타입 가드
굉장히 특수 문법 중 하나로, 내가 만든 함수가 if문 안에서 타입을 좁혀주도록 만드는 장치. 핵심은 반환 타입을 param is Type형태로 쓰는 것.
function isNumber(x: unknown): x is number {
return typeof x === "number";
}
const v: unknown = Math.random() > 0.5 ? 1 : "a";
if (isNumber(v)) {
v.toFixed(2); // v는 number로 좁혀짐
} else {
v.toUpperCase(); // v는 string으로 좁혀짐(여기선 남은 타입)
}
반환값이 boolean처럼 보이지만, 타입 시스템에겐 참이면 v는 number다라는 약속으로 작동한다.
하지만, 값의 타입까지는 보장하지 않기 때문에 실무에서는 더 엄격히 사용해야한다.
as로 가드처럼 보이게 만들지 말고, 정확한 프로퍼티 타입 체크를 섞어 주어야한다.
사용자 정의 타입 가드와 궁합이 좋은 filter
const arr: (string | null | undefined)[] = ["a", null, "b", undefined]; function isNonNullable<T>(v: T): v is NonNullable<T> { return v != null; } const cleaned = arr.filter(isNonNullable); // cleaned: string[]만약 단순히
const cleaned1 = arr.filter(v => v != null);사용 시,cleaned1: (string | null | undefined)[]이런식으로 그대로 남는다. 런타임에서는 알지만, 타입 시스템은 모르기 때문이다.따라서, 꼭 사용하는 것이 좋다.

