TypeScript - Handbook(More on Functions)

2021. 6. 1. 17:07프론트엔드/Typescript

728x90

오늘은 함수에 대한 응용을 공부해보자.

오늘 내용은 핸드북에 있는 내용만으로는 다소 이해가 어려울 수도 있어서,
직접 짠 코드들로 설명을 더할 예정이다.


함수형 타입을 표현하는 방법 (Function Type Expressions)

바로 코드부터 보자

function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}

function printToConsole(s: string) { console.log(s); }

greeter(printToConsole);

보다시피 string인자를 받으면 void를 반환하는 함수를 인자로 받는다.
표시는 '(인자명: 타입) => 반환_타입'로 한다.

여기서 중요한 부분이, 인자에 자료형만 넣으면 안된다.
(string) => void
로 하게되면, any 타입인 string이라는 인자가 되어버린다.


콜 시그니쳐 (Call Signatures)

이전 포스팅에서 말했듯, 자바스크립트에서는 모든 것이 객체이며,
property를 가질 수 있다.

그렇다면 property를 가지는 함수는 어떻게 정의할까?
답은 '함수도 오브젝트니까 오브젝트로 정의한다.'

type DescribableFunction = {
  description: string;  // property
  (someArg: number): boolean;  // callable
  (someArg: string): boolean;  // callable
};

function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

const test:DescribableFunction = (num: number): boolean => {
  return false;
}

test.description = "a";

// 출력: a returned false
doSomething(test);

사이트에 있는 예제만으로는 부족해서 이것저것 달아봤다.
우선 type을 정의할 때 Object를 정의하는 것처럼 정의한다.
그리고나서 하나의 property처럼 함수형태를 정의한다.
보이는 것처럼 오버로드도 가능하다.


생성자 시그니처 (Construct Signatures)

그렇다면 property도 있으니까 생성자도 있겠네?
왜냐하면 함수도 객체니까 ^___^ sibal

interface CallOrConstruct {
  new (s: string): Date; // 생성자 (Construct)
  (n?: number): number;  // 함수형태
  age: number; // 일반 property
}

// 함수로 사용하기
function testFunc(n?: number): void {
  console.log("Called: " + n!);
}

testFunc(10);

// property 사용
testFunc.age = 27;

// 생성자로 만들기
function testConstruct(s: string): Date {
  return new Date();
}

const testConstructInst: Date = new (testConstruct as any)("inho");
console.log(testConstructInst);

으 정말... 이해하기 위해 이것저것 많이 해봤다.

보다시피 CallOrConstruct은 하나의 '타입'인데,
생성자도 있고 함수형태도 있고 객체로서의 property도 있다.


제너릭을 이용하는 경우 (Generic Functions)

function firstElement<Type>(arr: Type[]): Type {
  return arr[0];
}

const ages: number[] = [27, 42, 35];
const firstAge: number = firstElement(ages);

const names: string[] = ["Inho", "Ahrum", "Jaeho"];
const firstName: string = firstElement(names);

보다시피 함수명과 인자 사이에 <> 태그로 넣어주면 된다.
예제에서는 number배열과 string배열을 넣어주었는데 각자 number와 string을 반환하였다.

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}

// Input은 string, Output은 number가 된다.
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

// 실제로은 이런 식으로 적용이 될 것이다.
function map<string, number>(arr: string[], func: (arg: string) => number): number[]
// 보다시피, 문자열과 함수를 넣으면 숫자 배열을 반환한다.

제너릭에 제약 추가하기 (Constraints)

제너릭이라고해도, 원하는 옵션이 있을 수 있다.
예를 들어 특정 클래스를 포함한다던가, 특정 property를 가진 Type만 사용하고 싶을 수 있다.

function longest<Type extends { length: number }>(a: Type, b: Type) {
  return a.length >= b.length ? a : b;
}

// 배열과 string 둘 다 length라는 property를 가지고 있기 때문에, Type으로 적용이 된다.
const longerArray = longest([1, 2], [1, 2, 3]);
const longerString = longest("alice", "bob");

// 하지만 number는 length property가 없기 때문에, Type에 해당하지 않는다.
// 에러남
const notOK = longest(10, 100);

그리고 여기서 골 때리는 일이 벌어진다.

function minimumLength<Type extends { length: number }>(
    obj: Type,
    minimum: number
  ): Type {
    if (obj.length >= minimum) {
      return obj;
    } else {
      // 에러 발생
      return { length: minimum };
    }
}

분명히 Type은 { length: number }를 포함하지만, { length: number } 그 자체가 Type은 아니다.
이 코드는 입력으로 들어오는 Type을 Union으로 직접 지정하는 것이 나아보인다 (무슨 목적의 코드인지 모르겠지만)

그리고 함수를 호출하는 부분에서 Type을 지정해줄 수 있다.

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

const arr = combine<string | number>([1, 2, 3], ["hello"]);

Type은 기본적으로 한가지로 지정이 되며, arr1이 number[]이기 때문에 Type이 number로 지정이 되었다.
그래서 Type이 string 또는 number이라는 것을 알리기 위해
함수명<타입1 | 타입2>(인자)
의 형태로 호출하였다.


제너릭을 사용하는 좋은 방법들 (Guidelines for Writing goood Generic Functions)

Type의 복잡도를 낮춰라.

// 이렇게 하면 반환 타입은 any가 된다. (arr이 any[]로 인식되기 때문).
// 그러므로 Typescript가 파악하는데 더 어려움이 생긴다.
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}

// 이런 식으로 타입을 최소한의 레벨로 낮추는 것이 좋다.
// 이 경우에는 반환형이 Type이 된다. (number[]가 들어왔다면, 반환값은 number)
function firstElement1<Type>(arr: Type[]){
  return arr[0];
}

인자 타입을 최소화 하라.

// 함수 인자 타입을 최소화 하라.
// 다음의 예시를 보면 func는 Type인자를 받아서 boolean으로 반환하고,
// 이 함수는 Type 배열과 함수를 받아서 Type[]를 반환한다.
// 하지만 여기서 Func라는 타입을 지정할 필요가 없다. 새로운 자료형이 필요한게 아니기 때문.
function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}

// 다음과 같이 Type은 하나로 받고, 인자(func)에 명세한다.
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}

Type은 두번 나와야 한다.

// 이 코드에서 제너릭이 필요할까?
// 제너릭을 사용할 때에는 반환을 하거나, 내부에서 해당 Type을 다시 사용해야할 때 사용한다.
function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}

// 이렇게 쓸 수 있다.
function greet(s: string) {
  console.log("Hello, " + s);
}

선택적 인자 (Optional Parameter)

대부분의 언어가 그렇듯 특정 인자를 받을수도, 안 받을수도 있다.

function f(x: number, y?: number) {
  // ...
}
f(); // NO
f(10); // OK
f(10, 20); // OK
f(10, 20, 30); // NO

x는 필수 인자고, y는 선택적 인자다. 선택적 인자 뒤에는 ?가 붙는다.
그래서 인자가 하나도 없거나 2개 초과인 경우는 사용이 불가하다.

기본적으로 y?:number라는 것은,
y:number | undefined
와 동일하다.

그래서 이런 코드도 가능하다

function f(x:number, y?:number) { }

// 인자 y는 number | undefined이므로, undefined를 넣어도 코드가 돌아간다.
f(10, undefined);

기본값 설정하기

함수 인자가 전달되지 않았을 때 기본값을 설정할 수 있다.

function f(x: number, y:number = 10) {
  // ...
}

다음과 같이 '변수 = 값'으로 기본값을 설정할 수 있다.


오버로드 (Function Overloads)

오버로드는 대부분의 언어에 있는, 함수 인자 재정의 함수다. 예를 들면 이런게 있다.

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}

 

몇가지 중요한 핵심이 있다.

1. 함수 정의는 모든 오버로드를 포함해야한다.

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;

// 함수 정의에서 위에서 정의된 세 인자(m, d, y)에 대한 부분을 포함하지 않는다.
// 에러!
function makeDate(mOrTimestamp: number, d?: number): Date {
  if (d !== undefined) {
    return new Date(mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}

2. 오버로드된 원형에 대해서만 호출이 가능하다

function makeDate(timestamp: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}

// 함수 정의에서는 3개의 인자를 받았기 때문에, 인자 2개만 받는 경우도 가능해보인다.
// 하지만 오버로드되지 않았기 때문에 불가능하다.
// 에러
makeDate(10, 10);

// 마찬가지로 인자 3개에 대한 오버로드도 없기 때문에, 인자 3개로 호출할 수도 없다.
// 에러
makeDate(10, 10, 10);

3. 오버로드의 인자 타입은 함수 정의와 같아야 한다.
단, any와 같이 포함하면 상관 없다

// 인자 타입이 같으면 변수명이 달라도 가능
function fn(something: boolean): void;

// 첫 인자가 함수 정의의 boolean형과 다르기 때문에 불가능
// 에러
function fn(x: string): void;

// 함수 정의
function fn(x: boolean) {}

함수 내부에서 this 사용하기 (Declaring this in a Function)

다음과 같이 멤버 함수 내부에서 this 키워드로 property에 접근할 수 있다.

const user = {
  id: 123,

  admin: false,
  becomeAdmin: function () {
    this.admin = true;
  },
};

알아두면 좋을 기타 내용들 (Other Types to Know About)

void

void는 함수가 값을 return하지 않을 때 return되는 것이다.
undefined와는 다른, 그냥 '아무것도 아니다'라는 뜻.

object

object는 Type이 대문자 Object가 아니라, 소문자 object라는 것만 기억하자.
(Object는 글로벌 타입. 예) Object.keys 등)

unknown

unknown은 any와 비슷하지만 any보다 더 안전하게 사용할 수 있다.
예를 들어 생각해보자. 회사에서 작은 상자 하나를 어딘가로 이동한다고 생각하자.
그럼 '그거 아무나한테 갖다 놓으라 그래'라고는 할 수 있지만,
'그거 모르는 사람한테 갖다 놓으라 그래'라고는 하지 않는 것과 같다. (좀 이상한가)

function f1(a: any) {
  a.b(); // OK
}
function f2(a: unknown) {
  // a가 unknown이기 때문에 더 조심스럽게 처리한다
  a.b();
}

never

never는 말 그대로 마주칠 일이 없는 녀석이다. narrowing에서도 봤듯, 일어날 수 없는 케이스를 처리한다.

1. 에러나 프로그램 종료일 때 사용

function fail(msg: string): never {
  throw new Error(msg);
}

2. 일어날 수 없는 케이스에 대해 처리

function fn(x: string | number) {
  if (typeof x === "string") {
    // do something
  } else if (typeof x === "number") {
    // do something else
  } else {
    x; // has type 'never'!
  }
}

Function

Function은 bind, call, apply등의 기능을 가진 글로벌 타입이다.
하지만 인자로 사용하게 되면, 반환 타입이 any가 되기 때문에 사용하지 않는게 좋다.


잔여 인자와 스프레드 문법 (Rest Parameters & Spread Syntax)

자바스크립트에는 '...'이라는 Spread Syntax가 있다. 나머지를 나타내는 뜻이다.

// Javascript
const a = { b: 3, c: 4, d: 5 };
const { b, ...etc } = a;

// 출력: { c: 4, d: 5 };
console.log(...etc)

// 배열
const list = [10, 20, 30];
const newList = [...list, 40];

// 출력: [10, 20, 30, 40]
console.log(newList);

TypeScript에서는 다음과 같이 사용한다.

function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}
// 변수 a = [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);

해체 대입 (Destructuring Assignment)

자바스크립트에는 해체 대입이라는게 있다. 오브젝트나 리스트에서 원하는 것만 변수에 대입하는 것이다.

// Javascript
const address = "서울 광진구 중대로 120";
const [city, area, road, num] = address.split(" ");

// 이렇게도 가능
const [city, area, ...etc] = address.split(" ");
const [city, area] = address.split(" ");

// 오브젝트
const inho = { age: 27, height: 178, weight: 64 };
const { age, height } = inho;

다음과 같이 사용할 수 있다.

function sum({ a, b, c }: { a: number; b: number; c: number }) {
  console.log(a + b + c);
}

//이렇게 그냥 똑같이 쓰면되는데, 아래와 같이 쓰면 깔끔해진다

type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
  console.log(a + b + c);
}

Assignabililty of Functions

여기 참 골때리는 내용이 있다.
void는 아무것도 리턴하지 않을 때 사용한다. 하지만 Contextual한 방식에서의 void는 반환을 막지는 않는다.

하지만 literal한 방식에서 void로 지정할 경우, 값을 반환을 할 수 없다.

// Contextual Typing
type vf = () => void
const f1:vf = function() {
  return true;
};

// v1의 Type은 void. 하지만 값은 true이다 (true가 반환되었기 때문)
const v1 = f1();

// literal type
function f2(): void {
  // 반환 Type이 void로 명시되었기 때문에, 그 무엇도 반환해서는 안 됨
  // 에러
  return true;
}

'프론트엔드 > Typescript' 카테고리의 다른 글

TypeScript - Handbook(Classes)  (0) 2021.06.22
TypeScript - HandBook(Object Types)  (0) 2021.06.08
TypeScript - Handbook(Narrowing)  (0) 2021.05.30
TypeScript - Handbook(Everyday Types)  (0) 2021.05.28
TypeScript - Handbook(The Basics)  (0) 2021.05.28