[ TypeScript ] TypeScript Handbook : Narrowing, More on Functions

·2023년 8월 8일
0

TypeScript

목록 보기
2/3
post-thumbnail

in 연산자

JavaScript에서는 개체 또는 해당 프로토타입 체인에 해당 속성이 있는지 확인하는 in연산자가 있다.
TypeScript에서는 잠재적 유형을 좁히는 방법으로 이 연산자를 사용할 수 있다.

type Fish = { swim : () => void };
type Bird = { fly : () =. void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim();
  }
  
  return animal.fly()
}

instanceof

JavaScript에는 값이 다른 값의 인스턴스인지 여부를 확인하는 연산자가 있다.
보다 구체적으로 JavaScript에서 x instanceof Foo는 x의 프로토타입 체인에 Foo.prototype이 포함되어 있는지 여부를 확인한다.

function logValue (x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString());
  } else {
    console.log(x.toUpperCase());
  }
}

never

never유형은 모든 유형에 할당할 수 있지만, 네버 유형에는 네버 자체를 제외하고는 다른 유형을 할당할 수 없다. 따라서 switch문에서 철저한 검사를 위해 사용할 수 있다.

type Shape = Circle | Square;

function getArea (shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default: 
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

함수 타입 표현식

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

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

greeter(printToConsole);

(a: string) => void라는 문법은 "문자열 타입 a를 하나의 매개변수로 가지고 반환값이 없는 함수"를 의미한다.

타입 별칭을 사용해서 함수의 타입에 이름을 붙일 수도 있다.

type GreetFunction = (a: string) => void;

function greeter(fn: GreetFunction) {
  // ...
}

호출 시그니처

Javascript에서 함수들은 호출이 가능할 뿐만 아니라, 프로퍼티도 가질 수 있다. 하지만 함수 타입 표현식 문법은 프로퍼티를 정의하는 것을 허락하지 않는다.
따라서 호출 가능하면서 프로퍼티를 가진 무언가를 설명하려고 하면 객체 타입에 호출 시그니처를 사용하여 표현할 수 있다.
이 문법은 함수 타입 표현식과 달리, 매개변수 타입과 반환값의 타입 사이에 =>가 아닌 :를 사용해야 한다.

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

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

구성 시그니처

Javascript 함수는 new 연산자를 통해서도 호출될 수 있다. TypeScript는 이런 것들이 주로 새로운 객체를 생성하는데 사용되기 때문에 생성자로 간주한다.
호출 시그니처 앞에 new 키워드를 붙임으로서 구성 시그니처를 작성할 수 있다.

type SomeConstructor = {
  new (s: string): SomeObject;
};

function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

제네릭 함수

입력값이 출력값의 타입과 관련이 있거나, 두 입력값의 타입이 서로 관련있는 형태의 함수를 작성하는 것은 흔히 일어나는 일이다.

function firstElement(arr) {
  return arr[0];
}

TypeScript에서 제네릭 문법은 두 값 사이의 상관관계를 표현하기 위해 사용된다. 함수 시그니처에서 타입 매개변수를 선언함으로써 제네릭 문법을 표현할 수 있다.

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

타입 매개변수 Type을 이 함수에 선언하고, 필요한 두 곳에 사용함으로써 함수의 입력값과 출력값 사이에 연결고리를 만들 수 있다.

추론 (Inference)

Type을 특정하지 않고 TypeScript에 의해 자동적으로 추론되도록 할 수 있다.
여러 개의 타입 매개변수도 사용할 수 있다.

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

const parsed = map(["1", "2", "3"], (n) => parseInt(n));

타입 제한 조건

위에서 모든 타입에 대해 동작하는 제네릭 함수를 작성했다. 두 값을 연관시키기 원하지만 특정한 값들의 부분집합에 한해서만 동작하기를 원할 때가 있는데, 이러한 경우 타입 제한 조건을 사용하여 타입 매개변수가 받아들일 수 있는 타입들을 제한할 수 있다.

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

// longerArray의 타입은 'number[]'이다.
const longerArray = longest([1,2], [1,2,3]);
// lognerString의 타입은 'alice' | 'bob'이다.
const longerString = longest('alice', 'bob');
// number에는 'length' 프로퍼티가 없으므로 에러가 난다.
// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
const notOK = longest(10, 100);

Type{length: number}로 제한했기 때문에 ab 매개변수에 대해 .length 프로퍼티에 접근할 수 있다. 타입 제한이 없다면 이런 값들이 length 프로퍼티를 가지지 않는 다른 타입일 수 있기 때문에 그 프로퍼티에 접근할 수 없었을 것이다.
결국 longest(10,100)은 number 타입이 .length 프로퍼티를 가지고 있지 않기 때문에 호출이 거부된 것이다.

제한된 값으로 작업하기

function minimumLength<Type extends {length: number}> (
	obj: Type,
    minimum: number
): Type {
      if(obj.length >= minimum) {
        return obj;
      } else {
        return {length: minimum};
        // Type '{ length: number; }' is not assignable to type 'Type'.
  		// '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
      }
}

Type{length: number}로 제한되어 있고, 함수는 Type이나 제약조건을 만족하는 값을 반환하므로 문제가 없는 것처럼 보인다.
문제는 이 함수가 제약사항을 만족하는 어떤 객체가 아닌, 입력된 어떤 객체를 반환한다는 점이다.
이 코드가 유효하다면, 확실히 동작하지 않을 아래의 코드를 작성할 수 있을 것이다.

// 'arr' gets value {length: 6}
const arr = minimumLength([1,2,3], 6);
// 여기서 배열은 'slice' 메서드를 가지고 있지만, 반환된 객체는 그렇지 않기에 에러가 발생한다.
console.log(arr.slice(0));

타입 인수를 명시하기

TypeScript는 제네릭 호출에서 의도된 타입을 대체로 추론해 내지만, 항상 그렇지는 않다.
일반적으로 짝이 맞지 않는 배열과 함께 해당 함수를 부르는 것은 잘못된 것이다.

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

const arr = combine([1,2,3], ["hello"]);
// Type 'string' is not assignable to type 'number'.

만약 이게 의도한 것이라면, 수동으로 Type을 명시해야 한다.

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

제네릭 함수 작성을 위한 가이드라인

타입 매개변수 누르기

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

function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}

// good
const a = firstElement1([1,2,3]);
// bad
const b = firstElement2([1,2,3]);

firstElement1 함수의 추론된 반환 타입은 Type이지만, firstElement2의 추론된 반환 타입은 TypeScript가 호출 중 타입을 해석하기 위해 기다리기 보단 호출 시점에 arr[0] 표현식을 타입 제한 조건을 이용해서 해석하기 때문에 any가 된다.

가능하다면 타입 매개변수를 제약하기 보다 타입 매개변수 그 자체를 사용하기

더 적은 타입 매개변수를 사용하기

// good
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}

// bad
function filter2<Type, Func extends (arg: Type) => boolean>(
 arr: Type[],
 func: Func
): Type[] {
   return arr.filter(func);
}

타입 매개변수 Func는 타입 인수를 원하는 호출자가 아무 이유 없이 추가 타입 인수를 제공해야 하기 때문에 상당히 좋지 않다. Func는 함수를 더 읽고 이해하기 어렵게 만들 뿐이지 아무것도 하지 않는다.

가능하다면 항상 타입 매개변수를 최소로 사용하기

타입 매개변수는 두 번 나타나야 한다

가끔 함수가 제네릭이 필요 없을 수 있다는 사실을 간과한다.

function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}

greet("world");

이를 보다 쉽게 작성할 수 있다.

function greet(s: string) {
  console.log("Hello, " + s);
}

타입 매개변수는 여러 값의 타입을 연관시키는 용도로 사용함을 기억해야 한다. 만약 타입 매개변수가 함수 시그니처에서 한 번만 사용되었다면, 어떤 것도 연관시키지 않고 있는 것이다.

만약 타입 매개변수가 한 곳에서만 나온다면, 정말로 필요한 것인지 다시 생각해보기

알아야 할 다른 타입

void

void는 값을 반환하지 않는 함수의 반환값을 의미한다. 함수에 return문이 없거나, 명시적으로 값을 반환하지 않을 때 추론되는 타입이다.

Javascript에서는 아무것도 반환하지 않는 함수는 암묵적으로 undefined 값을 반환한다. 하지만 TypeScript에서 voidundefined는 같은 것으로 간주되지 않는다.

object

특별한 타입 object는 원시값이 아닌 모든 값을 지칭한다. 빈 객체 타입 { }과는 다르고, 전역 타입 Object와도 다르다.
object는 Object가 아니므로 항상 object를 사용해야 한다!
Javascript에서 함수 값은 객체이다. 프로퍼티가 있고 프로토타입 체인에 Object.prototype이 있고, instanceof Object이면서 Object.keys를 호출할 수 있다. 이러한 이유료 TypeScript에서 함수 타입은 ojbect로 간주된다.

unknown

unknown 타입은 모든 값을 나타낸다. any와 유사하지만, unknown 타입에 어떤 것을 대입하는 것이 유효하지 않기 때문에 더 안전하다.

function f1(a: any) {
  a.b(); // OK
}

function f2(a: unknown) {
  a.b();
  // 'a' is of type 'unknown'.
}

any 형태의 값을 함수 본문에 사용하지 않고도 아무 값이나 받는 함수를 표현할 수 있기 때문에, 함수 타입을 설명하는 데에 유용하게 쓰인다.

반대로 unknown 타입의 값을 반환하는 함수를 표현할 수도 있다.

function safeParse(s: string): unknown {
  return JSON.parse(s);
}

const obj = safeParse(someRandomString);

never

어떤 함수는 결코 값을 반환하지 않는다.

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

never 타입은 결코 관측될 수 없는 값을 의미한다. 반환 타입에서는 해당 함수가 예외를 발생시키거나, 프로그램 실행을 종료함을 의미한다.

never은 TypeScript가 유니온에 아무것도 남아있지 않다고 판단했을 때 또한 나타난다.

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

Function

전역 타입 Functionbind, call, apply 그리고 Javascript 함수 값에 있는 다른 프로퍼티를 설명하는 데에 사용된다. Function 타입의 값은 언제나 호출될 수 있다는 값을 가지며, 이러한 호출은 any를 반환한다.

function doSomething(f: Function) {
  return f(1, 2, 3);
}
profile
개발을 개발새발 열심히➰🐶

0개의 댓글