4장 함수

2021. 07. 21

1. 함수 선언과 호출

  • 함수 parameter의 타입은 명시적으로 정의한다.
  • tsc는 함수의 body에서 사용된 타입들을 추론 하지만, 특별한 상황을 제외하고는 parameter 타입을 추론하지 않음
  • return type은 추론 하지만, 원하면 명시 할 수 있다. (일반적으로 return type을 tsc가 추론 할 수 있게 한다)
  • ts는 함수 생성자(new Function)을 제외하고는 모든 문법을 안전하게 지원한다.
  • 모든 문법은 매개변수 타입의 필수 어노 테이션, 반환 타입의 선택형 어노테이션에 적용하는 것과 같은 규칙을 따른다.
  • 타입스크립트에서 함수 호출시 타입 정보를 제공할 필요가 없으며, 인수 전달 시 tsc가 함수의 매개변수와 인수타입의 호환이 되는지 확인한다.

1) Parameter

  • 함수에서 ?를 이용해 선택적인 매개변수를 지정할 수 있다.
  • normal parameter 를 앞에, selectable parameter를 뒤에두고 사용한다.
function log(message: string, userId?: string) {
  let time = new Date().toLocaleTimeString();
  console.log(time, message, userId || 'Not signed In';
}
  • tsc에도 매개변수에 기본값을 지정할 수 있는데, 의미 상으로 매개 변수에 값을 전달하지 않아도 되므로 매개변수를 선택적으로 만드는 것과 같다.
  • 기본 매개 변수에도 타입을 지정할 수 있다.

2) rest parameter

  • js의 arguments를 사용하는 대신 rest parameter를 사용 한다.

3) Bind, Call, Apply

  • 사용할 때에도 사용한다.
  • tsc에서 사용할 때, strictBindCallApply 를 활성화 한다. strict 모드 활성화 시 이는 활성화된다.

4) this

  • ts에서 no-invalid-this 를 활성화 하여 조심히 사용하자
  • this 를 함수에서 사용할 때, 함수에 첫번째 인자로 우리가 기대하는 this 타입 을 넣어주도록 하자.
  • noImplicitThis 를 활성화 하여 함수에서 항상 this 타입을 명시적으로 설정하도록 강제하자.
  • 단, 위 옵션은 class와 메서드에서는 this 지정하라고 강제하지는 않는다.

5) 함수 시그니처

  • 함수 시그니처(타입 시그니처, 메소드 시그니처)는 functions 그리고 methods의 입력과 출력을 정의한다

시그니처는 다음을 포함한다:

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

여기서, sum 함수의 타입은 Function 이며, 이는 우리에게 큰 의미를 주지 않는다.

이외의 sum의 타입을 지정하는 방법으로,

(a: number, b:number) => number

이렇게 사용할 수 있는데, 이 코드는 타입 스크립트의 함수 타입 문법으로, type signature 또는 call signature라고 부른다.

call signature 의 특징으로

  • 타입 정보만 포함한다.
  • parameter type, return type 등을 명시해야 한다.

이 문법으로 함수의 타입을 지정할 수 있다.

type add = (a: number, b: number) => number

type Log = (message: string, userId?: string) => void
type Log = { (message: string, userId?: stirng): void }

6) 오버로드된 함수의 타입

  • 간단한 함수의 경우 단축형 시그니처를 사용하고, 복잡한 함수의 경우 전체 호출 시그니처를 사용하는게 좋다.
  • js는 오버로드시 매개변수의 수 뿐만 아니라 타입에 따라서도 동적으로 반환타입이 변경 될 수 있다.
type Reserve = {
    (from: Date, to: Date, destination: string): Reservation
    (from: Daete, destination: string): Reservation
}

const reserve: Reserve = (from, to, destination) => {
  ...
} // from, to는 any type, destination 은 never read 발생 가능
/**
  타입스크립트가 type signature overloading을 처리하는 방식에 의해 발생.
  f에 여러 개의 overload signature를 선언하면 호출자 관점에서 f의 타입은 overload signature의
  union type이 된다.
  f를 구현하는 관점에서 단일한 구현으로 조합된 타입을 나타낼 수 있어야 하며, 이 조합된 signature는
  자동으로 추론되지 않기 때문에 f를 구현할 때 선언을 해주어야 한다.
 */

const reserve: Reserve = (
  from: Date,
  toOrDestination: Date | string,
  destination?: string
) {
  if (toOrDestination instanceof Date && destination !== undefined) {
  } else if ( typeof toOrDestination === 'string') {
  }
}
  • 오버로드된 함수 타입을 선언시, 각 overload signature를 구현 signature에 할당할 수 있어야 한다.
  • 즉, overload를 할당할 수 있는 범위에서 구현 signature를 일반화 할 수 있어야 한다.

2. Polymorphism

  • concrete type : 기대하는 타입을 정확하게 알고, 실제 이 타입이 전달되었는지 확인할 때 유용하다. 하지만, 어떤 타입을 사용할지 미리 알 수 없는 상황에 함수를 특정 타입으로 제한하기 어렵다.
  • 이때, 사용되는게 generic 이다.

    • generic type parameter(T)는 여러 장소에 타입 수준의 제한을 적용할 때 사용하는 Placeholder type. polymorphic type parameter라고도 불린다.

type checker가 generic에 정의된 type을 보고, 타입을 추론해낸다.

function filter(array, f) {
  const result = [];
  for (let i=0; i<array.length; i++) {
    const item = array[i];
    if (f(item)) {
      result.push(item);
    }
  }
  return result;
}

type Filter = {
  (array: unknown, f: unknown) => unknown []
}

type Filter = {
  (array: number[], f: (item: number) => boolean): number []
  (array: string[], f: (item: string) => boolean): string []
  (array: object[], f: (item: object) => boolean): object []
}

const names = [
  { firstName: 'Kim' },
  { firstName: 'Park' },
  { firstName: 'Lee' }
];

const res = filter(
  names,
  _ => _.firstName.startWith('b') // error TS2339: firstName props isn't exist in 'object' type
)

type Filter = {
  <T> (array: T[], f: (item: T) => boolean): T[]
}
  1. filter 호출 시 tsc가 타입을 추론 해 줄것을 의미함.
  2. tsc는 전달된 array 타입을 보고 T의 타입 추론
  3. filter 호출 시점에 tsc가 T의 타입 추론해내면, filter에 정의된 모든 T를 추론한 타입으로 대체
  4. T 는 자리를 맡아 둔다는 의미의 placeholder type 이며, tsc가 문맥을 보고 placeholder type을 실제 타입으로 채운다.
  5. T는 filter의 type을 매개 변수화 한다.
  6. 이 때문에T를 제네릭 타입 매개 변수라고 부른다.
  7. tutorialsteacher
type Filter = {
  <T>(array: T[], f: (item: T) => boolean): T[];
}

</T> //

let filter:Filter = (array, fn) => //...

filter([1,2,3], _ => _ > 2) // => number로 한정
filter(['a','b','c'], _ => _ !== 'b') // => string 한정
const names = [
    { firstName: 'kim '},
    { firstName: 'kim '},
    { firstName: 'kim '},
    { firstName: 'kim '}
]

filter(names, _ => _.firstName.startsWith('b'));
  • 타입 스크립트는 전달된 인수의 타입을 이용해 제네릭을 어떤 타입으로 한정할 지 추론한다.
  • 제네릭은 함수의 기능을 더 일반화하여 설명할 수 있는 도구 이며, 제한 기능으로 생각 할 수 있다.
  • 제네릭 사용시, 타입 시그니처의 일부로써 <T> 를 앞에 선언 할 경우, 함수를 실제 호출할 때 concrete type을 T 로 한정한다. 이와 달리, T 의 범위를 Filter 의 타입 별칭으로 한정하려면 Filter를 사용할 때 타입을 명시적으로 한정해야 한다.
  • 일반적으로 tsc는 generic type을 사용하는 순간에 generic과 concrete type을 한정한다.

    • 함수를 호출, 클래스를 인스턴스화 할 때

1) 제네릭 타입이 한정되는 규칙

  • generic type의 선언 위치에 따라 타입의 범위 결정
  • generic type의 선언 위치에 따라 tsc가 타입을 언제 concrete type으로 한정하는지 결정된다.
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}

</T>

const filter: Filter = (array, f) => {//...}
};

이 예시에서 <T> 를 call signature의 일부 로써 선언했으므로, tsc는 filter type의 함수를 실제 호출 시 concrete typeT 로 한정한다.

이와 달리, T의 범위를 Filter의 type aliase로 한정하려면 Filter를 사용할 때 타입을 명시적으로 한정하게 해야 한다.

type Filter<T> = {
  (array: T[], f: (item: T) => boolean): T[]
}

const filter: Filter = (array, f) => {} // err TS2314
  • tsc는 generic type 사용하는 순간에 generic과 concrete type을 한정한다.
  • genenric type을 사용하는 순간

    • 함수를 호출 할 때
    • 클래스를 인스턴스화 할 때
    • type aliases와 interface에서는 이들을 사용하거나 구현할 때

2) 제네릭 선언 위치

  • tsc에서는 call signature를 정의하는 방법에 따라 제네릭을 추가하는 방법이 정해져 있다.
// 1
type Filter = {
	<T> (array: T[], f: (item: T) => boolean): T[]
}

</T>

/**
  T의 범위를 개별 시그니처로 한정한 전체 호출 시그니처
  Ts를 한 시그니처 범위로 한정했으므로,
  tsc는 filter 타입의 함수 호출시 이 시그니처의 T를 concrete type으로 한정
*/

// 2
type Filter<T> = {
  (array: T[], f: (item: T) => boolean): T[]
}

</T>
/**
  T의 범위를 모든 시그니처로 한정한 전체 호출 시그니처
  T를 filter type의 일부로써 선언했으므로, tsc는 filterType 함수 선언시 T로 한정
*/

// 3
type Filter = <T> (array: T[], f: (item: T) => boolean): T[];

</T>

// 4
type Filter<T> = (array: T[], f: (item: T) => boolean): T[];

</T>

// 5
function filter<T>(array: T[], f: (item: T) => boolean): T[];

/**
  T를 시그니처 범위로 한정한, 이름을 갖는 함수 호출 시그니처.
*/

type Map = <T, U> = (array: T[], f: (item: T) => U): U[];

3) 제네릭 타입 추론

  • 대부분 상황에서 tsc는 제네릭 타입을 추론해낸다.
  • 또한, 제네릭을 명시적으로 지정할 수 있다.

    • 모든 필요한 제네릭 타입을 명시하거나
    • 아무것도 명시 하지 않는다.
type Map = <T, U> (array: T[], f: (item: T) => U): U[];

map<string, boolean>(
  ['a','b','c'],
  _ => _ === 'a'
);

map<string> // TS2558 : 두개 타입 인수가 필요

map<string, boolean | string>

map<stirng, numbmer>(
  ['a','b','c'],
  _ => _ === 'a'
) // TS2322 'boolean' type은 number type에 지정 불가.
const promise = new Promise(resolve => {
  resolve(45)
})
promise.then(res => res * 4) // error TS2362

tsc는 제네릭 함수의 parameter에만 의지하여 generic type을 추론하는데 parameter에 대한 정보가 없으므로 T{} 로 간주한다.

const promise = new Promise<number>(resolve => {
  resolve(45)
})
promise.then(res => res * 4)

4) generic type aliases

type MyEvent<T> = {
  target: T
  type: string
}
  • type aliases에서 type aliases name과 = 사이, 저 <T> 위치에만 generic type을 선언 할 수 이따.
type ButtonEvent = MyEvent<HTMLButtonElement>

MyEvent 와 같은 제네릭 타입 사용시, 타입이 자동으로 추론되지 않으므로 type param을 명시적으로 한정해야 한다.

type TimedEvent<T> = {
  event: MyEvent<T>
  from: Date
  to: Date
}
  • generic type aliases를 함수 signature에도 사용 가능
function triggerEvent<T>(event: MyEvent<T>): void {}

5) 한정된 다형성

  • U타입은 적어도 T 타입을 포함하는 기능이 필요하다. 이런 상황을 U가 T의 상한 한계 라고 한다.
type TreeNode = {
    value: string
}

type LeafNode = TreeNode & {
    isLeaf: true
}

type InnerNode = TreeNode & {
    children: [TreeNode, TreeNode?]
}

type MapNode = <T extends TreeNode>(node: T, fn: (val: string) => string) => T;

</T>

/**
 위 처럼 사용 할 경우, 파라미터에 따라서 제네릭 타입이 결정된다.

 type MapNode<T extends TreeNode> = (node: T, fn: (val: string) => string) => T;
 이렇게 사용 될 경우 MapNode 함수 선언 시에 제네릭을 같이 입력해야 한다.
*/

const n: InnerNode = {
    value: 'userName',
    children: [{value: 'user'}, {value: 'name'}]
};

const b: TreeNode = {
    value: 'userName'
};

const c: LeafNode = {
    value: 'userName',
    isLeaf: true
};

const mapNode: MapNode  = (node, fn) => {
    node.value = fn(node.value);
    return node;
};

console.info(mapNode(c, _ => _.toUpperCase()));
  • 여기서 T 의 상한 경계는 TreeNode 이다. T 는 TreeNode 또는 그의 서브 타입이다.
  • mapNode 는 두 개의 매개변수를 받는데, 첫 번째 매개 변수는 T 타입의 노드이다.
  • 또한, mapNode 의 경우 T 타입의 노드를 반환한다.

⇒ T에 대한 upper bound 가 없는 경우에 T가 어떤 타입인지 추론이 불가능하고, 컴파일시에 안정성이 보장되지 않기 때문에 이 행위가 안전하지 않음을 알 수 있다.

여러 제한을 적용한 한정된 다형성

& 를 여럿 추가해서 작업을 진행하면 된다.

type HasSides = {
  numberOfSides: number
}

type SidesHaveLength = {
  sideLength: number
}

type Square = HasSides & SidesHaveLength

function logPerimeter<Shape extends HasSides & SidesHaveLength>(
  s: Shape
): Shape {
  console.log(s.numberOfSides * s.sideLength)
  return s
}

const s: Square = {
  numberOfSides: 4,
  sideLength: 5,
}

console.warn(logPerimeter(s))

한정된 다형성으로 인수 개수 정의

function call(
  f: (...argus: unknown[]) => unknown,
  ...args: unknown[]
): unknown {
  return f(...args)
}

function fill(length: number, val: string): string[] {
  return Array.from({ length }, () => val)
}

console.info(call(fill, 10, "a"))

이때, unknownnumber 를 할당 할 수 없다고 하는데, unknown 의 super set이 any 이고, args 의 하한 boundary가 정해져 있지 않기 때문에, fill에 해당 파라미터에 값을 할당 할 수 없다고 나온다.

이를 해결하기 위해, 제네릭에서 unknown 을 extends 하여 향후 할당될 타입에 대한 안정성을 보장해주는 방식으로 제네릭을 사용한다.

function call<T extends unknown[], R>(f: (...argus: T) => R, ...args: T): R {
  return f(...args)
}

function fill(length: number, val: string): string[] {
  return Array.from({ length }, () => val)
}

console.info(call(fill, 10, "a"))

제네릭타입의 기본값

type MyEvent<T extends HTMLElement = HTMLElement> = {
  target: T
  type: string
}
  • 특정요소 타입을 알 수 없을 때를 대비해 MyEvent 의 제네릭 타입에 기본값을 추가 할 수 있다.

© 2024 Doe의 devlog, Built with Vapor blog Theme Gatsby