TypeScript 특징 ( 함수, 배열, 인터페이스 )

김동현·2023년 7월 3일
0

typescript

목록 보기
2/4

타입스크립트 코드를 작성할 때 사용하는 자바스크립트의 주요 개념과 타입 시스템이 상호작용하는 방식을 자세히 알아보자.

타입스크립트에서 함수의 매개변수와 반환 타입을 유추하거나 명시적으로 선언하는 방법을 알아보고 타입스크립트 배열의 특징, 인터페이스와 클래스 사용법, 더 정확한 타입 작성을 위한 타입 제한자와 제네릭을 살펴보자.

⭐함수

🔷함수 매개변수

function sing(song){
  console.log(song)
}

song 매개변수를 제공하기 위해 의도한 값의 타입이 무엇일까?
명시적 타입 정보가 선언되지 않으면 절대 타입을 알 수 없다.

명시적 타입 정보가 선언되지 않으면 타입스크립트가 이를 any 타입으로 간주한다.
변수와 마찬가지로 타입스크립트를 사용하면 타입 애너테이션으로 함수 매개변수의 타입을 선언할 수 있다.

function sing(song: string){
    console.log(song);
}

🔸필수 매개변수

자바스크립트에서는 인수의 개수와 상관없이 함수를 호출할 수 있다.
하지만 타입스크립트는 함수에 선언된 모든 매개변수가 필수라고 가정한다.

function singTwo(first: string, second: string){
    console.log(first, second);
}

singTwo("I Will Survive", "Higher Love"); // ok

singTwo("Ball and Chain"); // 인수 1개 Error: Expected 2 arguments, but got 1.
singTwo('Go Your Own Way', "The Chain", "Dreams"); // 인수 3개 Error: Expected 2 arguments, but got 3.

매개변수가 2개라면 호출할 때 인자도 2개가 주어져야 한다.

함수 타입의 매개변수와 혼동하면 안된다.
"함수 타입의 매개변수가 2개라면 함수 정의할 때도 인자가 2개가 주어져야 한다" 라는 말로 이해하면 안된다.

type FuncType = (x: number, y: number) => void;
const func: FuncType = (x: number) => {
    //...
}

위 코드에서 FuncType은 두 개의 인자를 받아야하지만, 실제 정의된 func에서는 하나의 인자만 받는다.
하지만 이렇게 정의된 함수는 컴파일 타임에 오류를 발생시키지 않는다.
이는 타입스크립트에서 제공하는 타입 호환성 규칙에 따라 일부 인자를 생략할 수 있는 함수 타입의 반공변성 때문이다.
이러한 관례는 타입스크립트가 자바스크립트처럼 동적 언어로부터 파생되었기 때문에 발생한다.
이를 통해 과거의 자바스크립트 코드 및 라이브러리와의 호환성을 유지할 수 있으며, 개발자의 유연성을 보장한다.
하지만 이로 인해 여전히 안전하게 사용하고 싶을 때는, 필요한 인자를 모두 명시하는 것이 좋다.

type FuncType = (x: number, y: number) => void;
const func: FuncType = (x: number) => {
  console.log(x);
};
func(1); // Error: Expected 2 arguments, but got 1.
func(1, 2);

그래도 정의부 말고 호출할 때 인자 개수를 맞춰주지 않으면 타입 에러를 발생시킨다.

🔸선택적 매개변수

타입스크립트에서는 선택적 객체 타입 속성과 유사하게 타입 애너테이션의 : 앞에 ? 를 추가해 매개변수가 선택적이라고 표시한다.

function announceSong(song: string, singer?: string){
    console.log(song);
    if(singer){
        console.log(singer);
    }
}

announceSong("Greensleeves");
announceSong('Greensleeves', undefined);
announceSong("Chandelier", "Sia")

singer 매개변수의 타입이 string 타입이라고 선언했다.
하나의 인수를 사용해서 함수를 호출 하면 if(singer) 구문이 실행될 때 타입 에러가 발생할 것이라고 생각할 수 있다.

하지만 선택적 매개변수에는 항상 | undefined 가 유니언 타입으로 알아서 추가된다.
그렇기 때문에 위의 코드는 에러가 없는 유효한 코드이다.

let undefinedValue: undefined;
console.log(undefinedValue);

let stringValue: string;
console.log(stringValue); // Error: Variable 'stringValue' is used before being assigned.

위의 코드처럼 애초에 변수의 타입이 undefined 라면 초기화를 하지않아 자동으로 undefined 가 할당되더라도 사용할 때 에러가 나지 않는다.
타입 검사할 때 값과 타입이 동일하기 때문에 문제없이 사용이 가능하다.
같은 맥락으로 매개변수의 타입이 string | undefined 라서 undefined 가 자동으로 할당되더라도 singer 를 사용할 때 이미 undefined 가 할당되어 있다고 본다.
그래서 if(singer) 코드에서 에러가 발생하지 않는다.

선택적 매개변수를 정의하는건 ? 이지, undefined 가 아니다.

function announceSongBy(song: string, singer: string | undefined) {
  console.log(song);
  if (singer) {
    console.log(singer);
  } else {
    console.log(singer);
  }
}
announceSongBy("Greensleeves", undefined);
announceSongBy("Chandelier", "Sia");

announceSongBy("Greensleeves");
// Error: Expected 2 arguments, but got 1.

또한 선택적 매개변수는 필수 매개변수 뒤에 위치시켜야 한다.
그렇지 않으면 구문 오류가 발생한다.

🔸기본 매개변수

타입스크립트의 타입 추론은 초기 변수값과 마찬가지로 기본 함수 매개변수에 대해서도 유사하게 작동한다.
매개변수에 기본값이 있고 타입 애너테이션이 없는 경우, 타입스크립트는 해당 기본값을 기반으로 매개변수 타입을 유추한다.

기본 매개변수는 애초에 선택적 매개변수이기 때문에 ? 키워드는 사용하지 않는다.

function rateSong(song: string, rating = 0) {
  console.log(song, rating);
}

rateSong("Photograph");
rateSong("Set Fire to the Rain", 5);
rateSong("Set Fire to the Rain", undefined);

rateSong("At Last!", "100");
// Error: Argument of type 'string' is not assignable to parameter of type 'number'.

그리고 기본 매개변수에는 선택적 매개변수의 경우와 달리 자동으로 | undefined 유니언 타입을 추가하지 않는다.

function rateSong(song: string, rating = 0) {
  console.log(song, rating);
}

rateSong("Photograph");
rateSong("Set Fire to the Rain", 5);
rateSong("Set Fire to the Rain", undefined);

rating 의 기본값이 0이라서 두 번째 인자를 생략하면 자동으로 0이 할당된다.

그런데 더욱 충격적인건 명시적으로 undefined 타입을 인자로 담아 호출했을 경우이다.
매개변수로 보낸 undefined 값이 무시되고 기본값인 0이 할당된다.

function rateSong(song: string, rating:number | undefined = 0) {
  console.log(song, rating);
}

rateSong("Photograph");
rateSong("Set Fire to the Rain", 5);
rateSong("Set Fire to the Rain", undefined);

🔸나머지 매개변수

... 스프레드 연산자는 함수 선언의 마지막 매개변수에 위치하고, 해당 매개변수에서 시작해 함수에 전달된 "나머지" 인수가 모두 단인 배열에 저장되어야 함을 나타낸다.

function singAllTheSongs(singer: string, ...songs: string[]){
  for(const song of songs){
    console.log(song, singer);
  }
}
singAllTheSongs("Alicia Keys");
singAllTheSongs("Lady Gaga", "Bad Romance", "Just Dance");

singAllTheSongs("Ella Fitzgerald", 2000);
// Error: Argument of type 'number' is not assignable to parameter of type 'string'.

나머지 매개변수는 다음과 같이 사용할 수도 있다.

interface props {
  name: string;
  age: number;
  height: number;
}

function func({ name, ...rest }: props) {
  console.log(name, rest); // "kim",  {"age": 30, "height": 170} 
}

func({ name: "kim", age: 30, height: 170 });

다만, rest 에는 propsname 키를 뺀 나머지 키인 ageheight 키만 남아있다.
만약 ageheight 가 선택적 매개변수이고 호출 할때 name 키만 주어진 채로 호출하면 rest 는 빈 객체가 된다.

🔷반환 타입

함수가 반환할 수 있는 가능한 모든 값을 이해하면 함수가 반환하는 타입을 알 수 있다.

만약 함수에 다른 값을 가진 여러 개의 반환문을 포함하고 있다면, 타입스크립트는 반환 타입을 가능한 모든 반환 타입의 조합으로 유추한다.
다음 코드에서 getSongAt 함수는 string | undefined 를 반환하는 것으로 유추된다.

function getSongAt(songs: string[], index: number) {
  return index < songs.length ? songs[index] : undefined;
}

🔸명시적 반환 타입

타입 애너테이션을 사용해 함수의 반환 타입을 명시적으로 선언하는 것은 좋지 않다.

그러나 함수에서 반환 타입을 명시적으로 선언하는 방식이 유용할 때가 종종 있다.

  • 여러 타입을 반환하는 함수를 항상 동일한 타입의 값을 반환하도록 강제하여 코드의 안정성을 높힌다.
  • 타입스크립트는 재귀 함수의 반환 타입을 통한 타입 유추를 하지 않는다.
    명시적으로 타입스크립트에게 알려주면 타입 검사에 유용하다.
  • 수백 개 이상의 타입스크립트 파일이 있는 매우 큰 프로젝트에서 타입스크립트로 하여금 타입을 유추하게 하기보다 명시적으로 타입을 알려주면 타입스크립트 타입 검사 속도를 높일 수 있다.

잘못된 타입 반환

함수의 반환문이 함수의 반환 타입으로 할당할 수 없는 값을 반환하는 경우 타입스크립트는 할당 가능성 오류를 표시한다.

function getSongRecordingDate(song: string): Date | undefined {
  switch (song) {
    case "Strange Fruit":
      return new Date("April 20, 1939");
    case "Greensleeves":
      return "unknown"; // Error: Type 'string' is not assignable to type 'Date'.
    default:
      return undefined;
  }
}

재귀함수의 타입 반환

function singSongsRecursive(songs: string[], count = 0): number {
  return songs.length ? singSongsRecursive(songs.slice(1), count + 1) : count;
}

화살표 함수의 경우 => 앞에 배치한다.

const singSongsRecursive = (songs: string[], count = 0): number =>
  songs.length ? singSongsRecursive(songs.slice(1), count + 1) : count;

🔷함수 타입

자바스크립트에서 함수는 일급 객체이기 때문에 값으로 전달이 가능하다.
따라서 함수 자체를 나타내는 함수 타입이 있다.
함수 타입 구문은 화살표 함수 구문과 유사하지만 함수 본문 대신 타입이 있다는 점에 유의하자.
다음 inputAndOutput 변수 타입은 string[] 타입의 songsnumber 타입의 선택적 매개변수인 count 를 전달 받아 string 타입의 값을 반환하는 함수 타입임을 설명한다.

let inputAndOutput: (songs: string[], count?: number) => string;

함수 타입은 콜백 매개변수를 설명하는 데 자주 사용된다.

const songs = ["Juice", "Shake It Off", "What's Up"];

function runOnSongs(getSongAt: (index: number) => string): void {
  for (let i = 0; i < songs.length; i += 1) {
    console.log(getSongAt(i));
  }
}

function getSongAt(index: number): string {
  return `${songs[index]}`;
}

runOnSongs(getSongAt); // ok

function logSong(song: string): string {
  return `${song}`;
}

runOnSongs(logSong);
// Error: Argument of type '(song: string) => string' is not assignable to parameter of type '(index: number) => string'.
//  Types of parameters 'song' and 'index' are incompatible.
//      Type 'number' is not assignable to type 'string'.

두 함수를 서로 할당할 수 없다는 오류를 출력할 때 타입스크립트는 일반적으로 세 가지 상세한 단계를 제공한다.

  1. 첫 번째 들여쓰기 단계는 두 함수 타입을 출력한다.
  2. 다음 들여쓰기 단계는 일치하지 않는 부분을 지정한다.
  3. 마지막 들여쓰기 단계는 일치하지 않는 부분에 대한 정확한 할당 가능성 오류를 출력한다.

🔸함수 타입 괄호

괄호의 위치에 따라 의미가 달라지므로 주의하자.

// string | undefined 유니언 타입을 반환하는 함수 타입
let returnStringOrUndefined: () => string | undefined;

// 매개변수 없이 string 타입을 반환하는 함수 타입 | undefined 인 유니언 타입
let maybeReturnsString: (() => string) | undefined;

🔸매개변수 타입 추론

모든 함수에 대해 매개변수 타입을 선언해야 한다면 번거로울 것이다.
다행히도 타입스크립트는 선언된 타입의 위치에 제공된 함수의 매개변수 타입을 유추할 수 있다.

let singer: (song: string) => string;

singer = function (song) {
  return `${song}`;
};

콜백 함수의 매개변수 타입도 잘 유추할 수 있다.

const songs = ["Call Me", "Jolene", "The Chain"];

songs.forEach((song, index, that) => {
  return;
});

forEach() 두 번째 매개변수인 thisArgany 타입으로, 콜백 함수 내에 this 에 바인딩 된다.
하지만 화살표 함수 표현식을 사용하여 함수 인수를 전달하는 경우 thisArg 매개변수는 화살표 함수가 this 값을 렉시컬 ( lexical, 정적 ) 바인딩하기에 생략될 수 있다.

this 에 타입 오류 표시가 난 이유는 각기 다르다.

첫 번째 this 의 타입 오류를 보자.
'this' implicitly has type 'any' because it does not have a type annotation.
단순히 this 값의 타입이 any 이기 때문에 표시된 오류이다.

두 번째 this 의 타입 오류를 보자.
The containing arrow function captures the global value of 'this'.
화살표 함수내의 thisglobalThis 라는 것을 알려주고 있다.
다만, globalThis 가 아닌 undefined 값이 나온 이유는 타입스크립트가 "use strict" 모드이기 때문이다.

브라우저에서 실행해보면 globalThis 값이 출력된다.

🔸함수 타입 별칭

다음은 함수 타입 별칭을 이용해서 함수 타입을 지정하는 코드이다.

type StringToNumber = (input: string) => number;

let stringToNumber: StringToNumber;

stringToNumber = (input) => input.length;

stringToNumber = (input) => input.toUpperCase(); // Error: Type 'string' is not assignable to type 'number'.

🔷그 외 반환 타입

🔸void 반환 타입

return문이 없는 함수이거나 return; 로 값을 반환하지 않는 함수일 경우 어떤 값도 반환하지 않는다.
이렇게 반환 값이 없는 함수의 반환 타입을 void 키워드로 표시한다.

function logSong(song: string | undefined): void {
  if (!song) {
    return;
  }
  return true; // Error: Type 'boolean' is not assignable to type 'void'.
}

자바스크립트 함수는 값이 반환되지 않으면 기본적으로 undefined 를 반환하지만 voidundefined 와 동일하지 않다.
void 는 함수의 반환 타입이 무시된다는 것을 의미하고 undefined 는 반환되는 리터럴 값이다.

undefinedvoid 를 구분해서 사용하면 매우 유용하다.
빌트인 함수 및 메서드들의 콜백함수 반환 타입이 void 로 표시되어 있다면 다른 타입의 값을 반환하는 함수를 대입해도 그 반환 값들은 무시된다.
기능적으로 무시되는 것이 아니라 코드상 반환된 값을 사용하지 않았기 때문이다.

마찬가지로 개발자 본인도 void 타입을 반환하는 함수 타입을 선언했다면 그 자리에는 반환값이 없는 함수만을 대입하는 "규칙"을 지키도록 하자.
다음과 같이 "규칙"을 어기는 코드를 설계해도 타입에러가 발생하지 않는다는 점에 주의하자.

type NumberToVoid = (input: number) => void;

function useNumberToVoid(numberToVoid: NumberToVoid) {
  const result = numberToVoid(123);
  console.log(result);
}

useNumberToVoid((input) => {
  return input;
});

void 타입은 자바스크립트가 아닌 함수의 반환 타입을 선언하는 데 사용하는 타입스크립트 키워드이다.
void 타입은 함수의 반환값이 없고 사용하기 위한 것도 아니라는 표시임을 기억하자.

🔸never 반환 타입

반환값이 없을 뿐 아니라 반환할 생각이 전혀 없는 함수도 있다.
never 반환 함수는 의도적으로 항상 오류를 발생시키거나 무한 루프를 실행하는 함수이다.

함수가 절대 반환하지 않도록 의도하려면 명시적으로 never 타입 애너테이션을 추가해야 한다.

function fail(message: string): never {
  throw new Error("Invariant failure: ${message}");
}

function workWithUnsafeParam(param: unknown) {
  if (typeof param !== "string") {
    fail(`param should be a string, not ${typeof param}`);
  }
  // 여기에서 param의 타입은 string으로 알려진다.
  param.toUpperCase();
}

🔷함수 오버로드

일부 자바스크립트 함수는 선택적 매개변수와 나머지 매개변수만으로 표현할 수 없는 매우 다른 매개변수들로 호출될 수 있다.
이러한 함수는 오버로드 시그니처 ( overload signature ) 라고 불리는 타입스크립트 구문으로 설명할 수 있다.
함수 이름은 같지만 매개변수의 개수 및 타입과 반환 타입이 다른 함수를 여러 번 선언하고 하나의 최종 구현 시그니처 ( implementaion signature ) 를 선언한다.

function greet(name: string): void;
function greet(name: string, age: number): string;

function greet(name: string, age?: number): string | void {
  if (age !== undefined) {
    // age 매개변수가 존재하는 경우의 동작
    console.log(`안녕하세요, ${name}님! ${age}살이시네요.`);
    return "";
  } else {
    // age 매개변수가 존재하지 않는 경우의 동작
    console.log(`안녕하세요, ${name}님!`);
  }
}

함수 오버로드는 복잡하고 설명하기 어려운 함수 타입에 사용하는 최후의 수단이다.
함수를 단순하게 유지하고 가능하면 함수 오버로드를 사용하지 않는 것이 좋다.

다음은 함수 오버로드 대신 선택적 매개변수만을 사용한 코드이다.

function greet(name: string, age?: number) {
  if (age) {
    console.log(`안녕하세요, ${name}님! ${age}살이시네요.`);
  } else {
    console.log(`안녕하세요, ${name}님!`);
  }
}

선택적 매개변수만을 사용해도 충분한 것처럼 보인다.
그렇다면 함수 오버로드는 왜 필요한 걸까?

함수 오버로드는 선택적 매개변수만으로 처리하기 어려운 복잡한 매개변수 조합이나 다양한 동작을 처리해야 할 때 유용하다.
또한, 함수 오버로드를 사용하면 개발자가 함수를 호출할 때 예상치 못한 동작을 방지하기 위해 정적 타입 검사를 강화할 수 있다.
함수 오버로드를 사용하면 타입스크립트 컴파일러가 함수 호출 시 전달되는 인자의 타입을 체크하여 가장 적합한 시그니처를 선택할 수 있다.
이는 개발자가 실수로 잘못된 인자를 전달하여 예기치 않은 동작이 발생하는 것을 방지할 수 있다.

함수 오버로드의 또 다른 장점은 코드 가독성과 유지보수성을 향상시킨다는 것이다.
함수 오버로드를 사용하면 다양한 매개변수 조합에 대한 동작을 명시적으로 정의할 수 있기 때문에 코드를 이해하기 쉽고 예측하기 쉬워진다.
또한, 나중에 함수의 동작을 수정하거나 확장할 때도 함수 오버로드를 사용하여 기존 코드를 수정하지 않고 새로운 시그니처를 추가할 수 있다.

마지막으로, 선택적 매개변수만으로 처리할 수 있는 경우에도 함수 오버로드를 사용하면 일관성 있는 API 디자인을 구축할 수 있다.
함수 오버로드를 사용하여 다양한 매개변수 조합에 대한 시그니처를 명시적으로 정의함으로써 사용자에게 일관된 인터페이스를 제공할 수 있다.
이는 API의 사용성과 확장성을 개선하는 데 도움이 된다.

따라서 함수 오버로드는 선택적 매개변수만으로 처리하기 어려운 다양한 매개변수 조합과 동작 처리, 정적 타입 검사 강화, 코드 가독성과 유지보수성 향상, 일관된 API 디자인 등의 이유로 여전히 유용하고 필요한 개념이다.

그렇다면 함수 오버로드를 사용할때 각각의 시그니처마다 별도의 구현을 하지 않고 최종적으로 하나의 구현 시그니처만 정의하는 이유는 뭘까?

함수 오버로드를 사용하는 주요 목적은 코드의 가독성과 유지보수성을 높이는 것이다.
각각의 시그니처에 대해 독립적인 구현을 작성하면 코드가 중복되고 복잡해지며, 유지보수가 어려워질 수 있다.
또한, 각각의 시그니처에 대해 별도의 구현을 작성하는 경우, 코드 변경이 필요한 경우 모든 시그니처의 구현을 일일이 수정해야 하므로 작업량도 늘어난다.

위의 예제에서는 구현 시그니처에 모든 동작을 포함하고 있다.
함수 내부에서 age 매개변수의 존재 여부를 확인하여 분기하여 동작을 수행한다.
이렇게 하면 코드의 가독성이 좋아지고, 유지보수성도 향상된다.
또한, 코드 변경이 필요한 경우 구현 시그니처 하나만 수정하면 되므로 작업량이 줄어든다.

함수 오버로드를 사용하여 다양한 시그니처를 정의하고, 구현 시그니처에 동작을 통합하는 방식은 일관성 있는 API 디자인을 구축하고, 코드의 가독성과 유지보수성을 개선하는 데 도움을 준다.
따라서, 각각의 시그니처에 별도의 구현을 작성하는 것보다는 구현 시그니처에 통합하여 작성하는 것이 일반적으로 더 효율적이다.

🥓배열

자바스크립트의 배열은 매우 유연하고 내부에 모든 타입의 값을 혼합해서 저장할 수 있다.
그러나 대부분의 경우에 배열은 하나의 특정 타입의 값만 가진다.
배열의 값들의 타입을 통일시키지 않으면 버그를 야기할 수 있다.

타입스크립트는 배열에 어떤 데이터 타입이 있는지 기억하고, 배열이 해당 데이터 타입에 대해서만 작동하도록 제한한다.

// 타입: string[]
const warriors = ["Artemisia", "Boudica"];

warriors.push("Zenobia");
warriors.push(true); // Error

🔷배열 타입

배열에 대한 타입 애너테이션은 배열의 요소 타입 다음에 [] 가 와야 한다.

let arrayOfNumbers: number[];

배열 타입은 Array<number> 로도 작성할 수 있다.
하지만 대부분의 개발자들은 더 간단한 number[] 를 선호한다.
클래스와 제네릭은 뒤에서 살펴본다.

🔸배열과 함수 타입

// 타입: string 배열을 반환하는 함수 타입
let createStrings: () => string[];
// 타입 : string 을 반환하는 함수 배열 타입
let stringCreators: (() => string)[];

🔷유니언 타입 배열

// 타입: string 타입 또는 number 배열 타입
let stringOrArrayOfNumbers: string | number[];
// 타입: string 또는 number 를 아이템으로 가지는 배열 타입
let arrayOfStringOrNumbers: (string | number)[];

타입스크립트는 배열 타입을 가지는 변수에 타입 애너테이션을 명시하지 않으면 배열의 초기 값들의 모든 타입을 포함하는 유니언 타입 배열을 가진다.

// 타입: (string | null | undefined)[];
const nameMaybe = ["Aqualtune", "Blenda", undefined, null];

🔷any 배열의 진화

배열 타입을 가지는 변수가 빈 배열로 초기화 되면 그 배열은 any 타입 배열이 된다.
새로운 타입이 배열에 저장될 때마다 새로운 타입이 포함된 유니언 타입 배열로 변경된다.
이는 타입스크립트의 타입 검사를 무효화 하므로 지양하는 것이 좋다.

// 타입: any[]
let values = [];

values.push("");
// 타입: string[]
values;

values.push(0);

// 타입: (string | number)[]
values;

🔷다차원 배열

let arrayOfArraysOfNumbers: number[][];
// 또는 (number[])[] 로 나타낼 수 있다.

arrayOfArraysOfNumbers = [
  [1, 2, 3],
  [4, 5, 6],
];

🔷배열 멤버

타입스크립트는 배열의 멤버를 찾아서 해당 배열의 타입 요소를 되돌려주는 전형적인 인덱스 기반 접근 방식을 이해하는 언어이다.

const defenders = ["Clarenza", "Dina"];

// 타입: string
const defender = defenders[0];

const soldiersOrDates = ["Deborah Sampson", new Date(1782, 6, 3)];

// 타입: string | Date
const soldiersOrDate = soldiersOrDates[0];

🔸주의 사항: 불안정한 멤버

다음 코드는 타입스크립트 컴파일러의 기본 설정에서 오류를 표시하지 않는다.

function withElements(element: string[]) {
  console.log(element[9001].length);
}

withElements(["It's", "over"]);

타입스크립트는 검색된 배열의 멤버가 존재하는지 의도적으로 확인하지 않는다.

타입스크립트에는 배열 조회를 더 제한하고 타입을 안전하게 만드는 noUncheckedIndexedAccess 플래그가 있지만 이 플래그는 매우 엄격해서 대부분의 프로젝트에서 사용하지 않는다.

🔷스프레드와 나머지 매개변수

... 연산자를 사용하는 나머지 매개변수와 배열 스프레드는 자바스크립트에서 배열과 상호작용하는 핵심 방법이다.
타입스크립트는 두 방법을 모두 이해한다.

🔸스프레드

// 타입: string[]
const soldiers = ["Harriet Tubman", "Joan of Arc", "Khutulun"];
// 타입: number[]
const soldierAges = [90, 19, 45];
// 타입: (string | number)[]
const conjoined = [...soldiers, ...soldierAges];

// 타입: (string | number)[]
const numberAndString = [5, "string1", "string2"];
// num 타입 : string | number
// strs 타입 : (string | number)[]
const [num, ...strs] = numberAndString;

🔸나머지 매개변수 스프레드

function logWarriors(greeting: string, ...names: string[]) {
  for (const name of names) {
    console.log(`${greeting}, ${name}!`);
  }
}

const warriors = ["Cathay Williams", "Lozen", "Nzinga"];

logWarriors("Hello", ...warriors);

const birthYears = [1844, 1840, 1583];

logWarriors("Born In", ...birthYears);
// Error: Argument of type 'number' is not assignable to parameter of type 'string'.

🔷튜플

자바스크립트 배열은 이론상 어떤 크기라도 될 수 있다.
하지만 때로는 튜플이라고 하는 고정된 크기의 배열을 사용하는 것이 유용하다.
튜플 배열은 각 인덱스에 알려진 특정 타입을 가지며 배열의 모든 가능한 멤버를 갖는 유니언 타입보다 더 구체적이다.

let yearAndWarrior: [number, string];

yearAndWarrior = [530, "Tomyris"]; // ok

yearAndWarrior = [false, "Tomyris"]; // Error: Type 'boolean' is not assignable to type 'number'.

yearAndWarrior = [530]; 
// Error: Type '[number]' is not assignable to type '[number, string]'.
//          Source has 1 element(s) but target requires 2.

배열 구조 분해 할당을 사용할 때 리터럴 배열을 분해하면 튜플 배열로 취급되지만 변수 배열을 분해하면 일반 배열로 취급된다.

const a = [340, "Archidamia"];
const b = [1828, "Rani of Jhansi"];
// year1 타입: string | number
// warrior1 타입: string | number
let [year1, warroir1] = Math.random() > 0.5 ? a : b;

// year2 타입: string | number | boolean
// warrior2 타입: string | number | undefined
let [year2, warroir2] = Math.random() > 0.5 ? [true, undefined] : b;

// year3 타입: number
// warrior3 타입: string
let [year3, warroir3] =
  Math.random() > 0.5 ? [340, "Archidamia"] : [1828, "Rani of Jhansi"];

🔸튜플 할당 가능성

가변 길이의 배열 타입은 튜플 타입에 할당할 수 없다.

// 타입: (number | boolean)[]
const pairLoose = [false, 123];

const pariTupleLoose: [boolean, number] = pairLoose;
// Error: Type '(number | boolean)[]' is not assignable to type '[boolean, number]'.
//          Target requires 2 element(s) but source may have fewer.

나머지 매개변수로서의 튜플

튜플은 구체적인 길이와 요소 타입 정보를 가지는 배열로 간주되므로 함수에 전달할 인수를 저장하는 데 특히 유용하다.

function logPair(name: string, value: number) {
  console.log(`${name} has ${value}`);
}
// 타입: (string | number)[]
const pairArray = ["Amage", 1];

logPair(...pairArray);
// Error: A spread argument must either have a tuple type or be passed to a rest parameter.

const pairTuple: [string, number] = ["Amage", 1];
logPair(...pairTuple);

🔸튜플 추론

타입스크립트는 생성된 배열을 튜플이 아닌 가변 길이의 배열로 취급한다.
따라서 배열이 변수의 초기값 또는 함수에 대한 반환값으로 사용되는 경우, 가변 길이의 배열로 가정한다.

function firstCharAndSize(input: string) {
  return [input[0], input.length];
}
// firstChar 타입: string | number
// size 타입: string | number
const [firstChar, size] = firstCharAndSize("Gudit");

타입스크립트에서는 튜플 타입이어야 함을 다음 두 가지 방법으로 나타낸다.

  • 명시적 튜플 타입
  • const 어서션 ( assertion )

명시적 튜플 타입

function firstCharAndSize(input: string): [string, number] {
  return [input[0], input.length];
}
// firstChar 타입: string
// size 타입: number
const [firstChar, size] = firstCharAndSize("Gudit");

const 어서션

명시적 타입 애너테이션에 튜플 타입을 입력하는 작업은 매우 귀찮을 수 있다.
그 대안으로 타입스크립트는 값 뒤에 넣을 수 있는 const 어서션인 as const 연산자를 제공한다.
const 어서션은 리터럴 뒤에 배치되며, 타입스크립트에 타입을 유추할 때 읽기 전용이 가능한 값 형식을 사용하도록 지시한다.
다음과 같이 배열 리터럴 뒤에 as const 가 배치되면 배열이 튜플로 처리되어야 함을 나타낸다.

function firstCharAndSize(input: string) {
  return [input[0], input.length] as const;
}
// firstChar 타입: string
// size 타입: number
const [firstChar, size] = firstCharAndSize("Gudit");

"읽기 전용" 에서 눈치 챘다시피 const 어서션은 튜플로의 전환하는 것을 넘어서 값 수정 또한 불가능하게 만든다.
따라서 타입도 일반 튜플 타입과 다르다.
위의 코드처럼 배열 분해 할당을 위해 사용하면 상관없지만 읽기 전용 튜플 타입은 일반 튜플 타입에 할당하려면 타입에러가 발생한다.

const pairAlsoMutable1: [number, string] = [1157, "Tomoe"] as const;
// Error: The type 'readonly [1157, "Tomoe"]' is 'readonly' and cannot be assigned to the mutable type '[number, string]'.

const pairAlsoMutable2: readonly [1157, "Tomoe"] = [1157, "Tomoe"] as const; // ok

pairAlsoMutable2[0] = 123;
// Error: Cannot assign to '0' because it is a read-only property.

🧂인터페이스

인터페이스는 연관된 이름으로 객체 형태를 설명하는 또 다른 방법이다.
인터페이스는 별칭으로 된 객체 타입과 여러 면에서 유사하지만 일반적으로 더 읽기 쉬운 오류 메시지, 더 빠른 컴파일러 성능, 클래스와의 더 나은 상호 운용성을 위해 선호된다.

🔷타입 별칭 vs 인터페이스

type PoetType = {
  born: number;
  name: string;
};

interface PoetInterface {
  born: number;
  name: string;
}

두 구문은 거의 같으나 살짝 다른 부분이 보인다.
타입 별칭은 세미콜론을 사용해 변수를 선언하는 것과 같고 인터페이스는 세미콜론 없이 클래스 또는 함수를 선언하는 것과 같다.
type 타이명 = {...};
interface 인터페이스명 {...}
인터페이스에 대한 타입스크립트의 할당 가능성 검사와 오류 메시지는 객체 타입에서 실행되는 것과 거의 동일하다.
그러나 몇 가지 주요 차이점이 있다.

  • 인터페이스는 속성 증가를 위해 병합 ( merge ) 할 수 있다.
    이 기능은 내장된 전역 인터페이스 또는 npm 패키지와 같은 외부 코드를 사용할 때 특히 유용하다.
  • 인터페이스는 클래스가 선언된 구조의 타입을 확인하는 데 사용할 수 있지만 타입 별칭은 사용할 수 없다.
  • 일반적으로 인터페이스에서 타입스크립트 타입 검사기가 더 빨리 작동한다.
    인터페이스는 타입 별칭이 하는 것처럼 새로운 객체 리터럴의 동적인 복사 붙여넣기보다 내부적으로 더 쉽게 캐시할 수 있는 명명된 타입을 선언한다.
  • 인터페이스는 이름 없는 객체 리터럴의 별칭이 아닌 이름있는(명명된) 객체로 간주되므로 어려운 특이 케이스에서 나타나는 오류 메시지를 좀 더 쉽게 읽을 수 있다.

타입 별칭의 유니언 타입과 같은 기능이 필요하지 않는 한, 인터페이스를 사용하는 것이 좋다.

🔷속성 타입

🔸선택적 속성

객체 타입과 마찬가지로 타입 애너테이션 : 앞에 ? 를 사용해 선택적 속성을 나타낼 수 있다.

interface Book {
  author?: string;
  pages: number;
}

const ok: Book = {
  author: "Rita Dove",
  pages: 80,
};

const missing: Book = {
  pages: 80,
};

🔸읽기 전용 속성

경우에 따라 인터페이스에 정의된 객체의 속성을 재할당하지 못하도록 인터페이스 사용자를 차단하고 싶다.
타입스크립트는 속성 이름 앞에 readonly 키워드를 추가해 다른 값으로 설정될 수 없음을 나타낸다.

interface Page {
  readonly text: string;
}

function read(page: Page) {
  console.log(page.text);
  
  page.text += "!";
  // Error: Cannot assign to 'text' because it is a read-only property.
}

page.text 를 수정하는 것은 에러를 표시하지만 Page 타입의 변수에 객체를 할당하는 것은 에러를 표시하지 않는다.

interface Page {
  readonly text: string;
}

const page1 : Page = {
  text : "text",
}

let page2 : Page;
page2 = {text: "abc"};

쓰기 가능한 ( writable ) 속성은 readonly 속성에 할당될 수 있다.

interface Page {
  readonly text: string;
}
// Page 타입이 아닌 유추된 객체 타입이다.
const noPageType = {
  text: "Hello, world",
};
// 따라서 속성의 수정이 자유롭다.
noPageType.text += "!";

// Page 타입 객체 선언
let pageType: Page;
// Page 타입에 할당이 가능하다.
pageType = noPageType;

pageType.text += "!";
// Error: Cannot assign to 'text' because it is a read-only property.

readonly 는 타입 시스템 구성 요소일 뿐 컴파일된 자바스크립트 출력 코드에는 존재하지 않는다.
단지 타입스크립트 타입 검사기를 사용해 개발 중에 그 속성이 수정되지 못하도록 보호하는 역할을 한다.

참고로 noPageType 객체가 Page 타입에 정의된 속성을 모두 포함하고 있으면 더 많은 속성이 있더라도 할당이 가능하다.

const noPageType = {
  text: "Hello, world",
  subtext: "subtext",
};

let pageType: Page;

pageType = noPageType;
pageType.subtext;
// Error: Property 'subtext' does not exist on type 'Page'.

다만, Page 타입으로 선언된 pageType 는 타입에 정의된 속성에만 접근 가능하다.

🔸함수와 메서드

타입스크립트에서 인터페이스 멤버를 함수 타입으로 선언할 수 있다.

  • 메서드 구문: 인터페이스 멤버를 member(): void 와 같이 객체의 멤버로 호출되는 함수로 선언
  • 속성 구문: 인터페이스 멤버를 member: () => void 와 같이 독립 함수와 동일하게 선언
interface HasBothFunctionTypes {
  property: () => string;
  method(): string;
}

const hasBoth: HasBothFunctionTypes = {
  property: () => "",
  method() {
    return "";
  },
};
hasBoth.property(); // ok
hasBoth.method(); // ok

두 가지 방법 모두 ? 를 사용해 선택적 멤버로 나타낼 수 있다.
메서드 구문에는 ? 키워드를 () 앞에 붙이는 게 조금 다를 뿐이다.

interface HasBothFunctionTypes {
  property?: () => string;
  method?(): string;
}

메서드 구문과 속성 구문의 차이점은 다음과 같다.

  • 메서드 구문은 readonly로 선언할 수 없지만 속성 구문은 가능하다.
  • 인터페이스 병합은 메서드와 속성을 다르게 처리한다.
  • 타입에서 수행되는 일부 작업은 메서드와 속성을 다르게 처리한다.

현 시점에서 추천하는 스타일 가이드는 다음과 같다.

  • 기본 함수가 this 를 참조할 수 있다는 것을 알고 있다면 메서드 함수를 사용하자.
    가장 일반적으로 클래스의 인스턴스에서 사용된다.
  • 반대의 경우는 속성 함수를 사용하자.

🔸호출 시그니처

인터페이스와 객체 타입은 호출 시그니처로 선언할 수 있다.
호출 시그니처는 값을 함수처럼 호출하는 타입 시스템에 대한 방식이다.
호출 시그니처가 선언한 방식으로 호출되는 값만 인터페이스에 할당할 수 있다.
즉, 할당 가능한 매개변수와 반환 타입을 가진 함수이다.

type FunctionAlias = (input: string) => number;
interface CallSignature {
  (input: string): number;
}
// 타입 : (input: string) => number
const typeFunctionAlias: FunctionAlias = (input) => input.length;
// 타입 : (input: string) => number
const typedCallSignature: CallSignature = (input) => input.length;

객체의 함수 멤버와 호출 시그니처를 구별해야 한다.

// 객체 인터페이스
interface HasFunctionType{
    method(): string;
}
// 호출 시그니처 인터페이스
interface HasCallSignatureType{
    ():string;
}
// 객체를 할당
const hasFunctionType: HasFunctionType = {
    method() {return ""}
}
// 함수 객체를 할당
const hasCallSignatureType: HasCallSignatureType = () => "";

const hasCallSignatureType2: HasCallSignatureType = { () => "" }; 
// Error: This expression is not callable.
//  Type '{}' has no call signatures.

인터페이스에 함수 멤버가 정의되어있다면 동일한 이름의 함수 멤버를 가진 객체를 할당하면 된다.
하지만 호출 시그니처로 선언된 인터페이스는 객체를 할당하는 것이 아니다.
() 로 호출가능한 표현식이 와야 한다.

호출 시그니처는 사용자 정의 속성을 추가로 갖는 함수를 설명하는 데 사용할 수 있다.

interface FunctionWithCount {
  count: number;
  (): void;
}

const keepsTrackOfCalls = () => {};
keepsTrackOfCalls.count = 0;

const hasCallCount: FunctionWithCount = keepsTrackOfCalls;

keepsTrackOfCalls 는 호출 가능한 객체(함수) 이므로 호출 시그니처로 선언된 인터페이스에 할당이 가능하다.
그런데 FunctionWithCount 는 호출 시그니처로 선언된 인터페이스일 뿐만 아니라 count 라는 일반 속성도 가지고 있다.
따라서 이 인터페이스에 할당하기 위해서는 함수 객체 내부에 count 속성도 가지고 있어야 한다.
위의 조건들 중 하나라도 만족하지 못하면 타입 에러가 발생한다.

위의 함수 객체를 보면 count 속성을 동적으로 추가했다.
일반 객체일 경우엔 동적으로 속성을 추가하면 타입 오류가 발생된다.

만약 keepsTrackOfCalls 의 타입을 명시적으로 지정했다면 타입에 정의되지 않은 속성을 동적으로 추가하면 타입 에러가 발생한다.
위의 코드에서는 keepsTrackOfCalls 의 타입을 명시적으로 선언하지 않았기에 동적으로 속성을 추가해도 에러가 나지 않은 것이다.

일반 객체와 함수 객체 간의 동적 속성 추가에 대한 차이점은 타입스크립트의 타입 추론 방식과 관련이 있다.

일반 객체의 경우, 타입스크립트는 속성이나 메서드를 동적으로 추가하는 것을 허용하지 않는다.
이는 타입 안정성을 보장하기 위한 것이다.
타입스크립트에서는 객체의 타입은 정적으로 추론되며, 타입 체크를 통해 객체의 구조와 속성이 일치하는지 확인한다.
따라서 일반 객체에 동적으로 속성을 추가하면 해당 속성은 정적 타입 체크에 포함되지 않으므로, 타입 에러가 발생한다.

함수 객체의 경우, JavaScript에서 함수는 객체로 취급될 수 있다.
타입스크립트는 이러한 동작을 반영하여 함수 객체에도 동적 속성 추가를 허용한다.
함수 객체에 속성을 추가하면 해당 속성은 동적으로 함수 객체에 존재하는 속성으로 취급된다.
타입스크립트는 이러한 동적 속성을 함수 객체의 정적 타입에 포함하지 않기 때문에, 타입 에러가 발생하지 않는다.

즉, 함수 객체에 동적으로 속성을 추가할 수 있는 것은 타입스크립트에서 함수를 객체처럼 사용할 수 있도록 유연성을 제공하기 위함이다.
그러나 이러한 유연성은 객체의 동적 속성 추가에는 적용되지 않는다.

🔸인덱스 시그니처

일부 자바스크립트 프로젝트는 임의의 string 키에 값을 저장하기 위한 객체를 생성한다.
이러한 컨테이너 ( container ) 객체의 경우 모든 가능한 키에 대해 필드가 있는 인터페이스를 선언하는 것은 불가능하다.
따라서 타입 검사를 만족하면서 동적으로 속성을 만들 수 있도록 하는 인덱스 시그니처를 알아보자.

interface WordCounts {
  [i: string]: number;
}

const counts: WordCounts = {};

counts.apple = 0; // ok;
counts.banana = 1; // ok;

counts.cherry = false;
// Error: Type 'boolean' is not assignable to type 'number'.

인덱스 시그니처 구문을 통해 인터페이스의 객체가 임의의 키를 받고, 해당 키에 매칭하는 특정 타입을 반환할 수 있음을 나타낸다.
위의 코드에서는 string 타입의 키를 동적으로 받고 number 타입의 값을 반환한다.

자바스크립트 객체 속성 조회 ( lookup ) 는 암묵적으로 키를 문자열로 변환하기 때문에 인터페이스의 객체는 일반적으로 문자열 키를 사용한다.

인덱스 시그니처는 객체에 값을 할당할 때 편리하지만 타입 안정성을 완벽하게 보장하지는 않는다.

interface DatesByNames {
  [i: string]: Date;
}
const publishDates: DatesByNames = {
  Frankenstein: new Date("1 January 1818"),
};

publishDates.Frankenstein; // 타입: Date
publishDates.Frankenstein.toDateString(); // ok

publishDates.Beloved; // 타입: Date, 그러나 값은 undefined
publishDates.Beloved.toDateString(); // 타입 시스템에서는 오류가 나지 않지만 런타임에는 오류가 발생함

속성과 인덱스 시그니처 혼합

인터페이스는 명시적으로 명명된 속성과 포괄적인 용도의 string 인덱스 시그니처를 한 번에 포함할 수 있다.
단, 각각의 명명된 속성의 타입은 포괄적인 용도의 인덱스 시그니처로 할당할 수 있어야 한다.

interface HistoricalNovels{
    Oroonoko: string;
    // Error: Property 'Oroonoko' of type 'string' 
    //        is not assignable to 'string' index type 'number'.
    [i:string]: number;
}

또한 명명된 속성이 없다면 할당될 수 없다.

interface HistoricalNovels{
    Oroonoko: number;
    [i:string]: number;
}

const novels: HistoricalNovels = {
    Outlander: 1991,
    Oroonoko: 1688
}

const missingOroonoko: HistoricalNovels = {
    Outlander: 1991
}
// Error: Property 'Oroonoko' is missing in type '{ Outlander: number; }' 
//        but required in type 'HistoricalNovels'.

속성과 인덱스 시그니처를 혼합해서 사용하는 일반적인 타입 시스템 기법 중 하나는 인덱스 시그니처의 원시 속성보다 명명된 속성에 대해 더 구체적인 속성 타입 리터럴을 사용하는 것이다.

interface ChapterStarts {
  preface: 0;
  [i: string]: number;
}

const correctPreface: ChapterStarts = {
  preface: 0,
  night: 1,
  shopping: 5,
};

const wrongPreface: ChapterStarts = {
  preface: 1,
  // Error: Type '1' is not assignable to type '0'.
  night: 1,
  shopping: 5,
};

숫자 인덱스 시그니처

자바스크립트가 암묵적으로 객체 속성 조회 키를 문자열로 변환하지만 때로는 개체의 키로 숫자만 허용하는 것이 바람직할 수 있다.
타입스크립트 인덱스 시그니처는 키로 string 대신 number 타입을 사용할 수 있지만, 포괄적인 용도의 string 인덱스 시그니처의 타입으로 할당할 수 있어야 한다.

interface MoreNarrowNumbers {
    [i:number]: number;
    [i:string]: string | number;
}

interface MoreNarrowNumbers2 {
    [i:number]: number;
    // Error: 'number' index type 'number' is not assignable to 'string' index type 'string'.
    [i:string]: string;
}

그 이유는 number 타입의 키로 속성을 추가해도 결국 string 타입으로 변환되기 때문이다.

interface MoreNarrowNumbers {
  [i: number]: number;
  [i: string]: string | number;
}

let mixesNumbersAndStrings: MoreNarrowNumbers = {
  0: 123,
};
mixesNumbersAndStrings[1] = 456;
mixesNumbersAndStrings.key1 = "value1";

console.log(mixesNumbersAndStrings); // {"0": 123, "1": 456, "key1": "value1"}

🔸중첩 인터페이스

인터페이스 타입도 자체 인터페이스 타입 혹은 객체 타입을 속성으로 가질 수 있다.

interface Novel {
  author: {
    name: string;
  };
  setting: Setting;
}

interface Setting {
  place: string;
  year: number;
}

🔷인터페이스 확장

타입스크립트는 인터페이스가 다른 인터페이스의 모든 멤버를 복사해서 선언할 수 있는 확장된 ( extend ) 인터페이스 를 허용한다.

interface Writing {
  title: string;
}

interface Novella extends Writing {
  pages: number;
}

let myNovvella: Novella = {
  pages: 195,
  title: "Ethan Frome",
};

확장된 인터페이스의 속성 개수보다 적거나 많으면 타입 에러가 발생한다.

🔸재정의된 속성

인터페이스를 확장하면서 속성을 재정의 ( override ) 하거나 대체할 수 있다.
재정의된 속성은 기존의 속성에 할당되는 타입으로만 가능하다.

interface WithNullableName {
  name: string | null;
}

interface WithNonNullableName extends WithNullableName {
  name: string;
}

interface WithNumericName extends WithNullableName {
  name: number | string;
}
// Error
// Interface 'WithNumericName' incorrectly extends interface 'WithNullableName'.
//   Types of property 'name' are incompatible.
//     Type 'string | number' is not assignable to type 'string | null'.
//       Type 'number' is not assignable to type 'string'.

🔸다중 인터페이스 확장

타입스크립트의 인터페이스는 여러 개의 다른 인터페이스를 확장해서 선언할 수 있다.

interface GivesNumber {
  giveNumber(): number;
}

interface GivesString{
  giveString(): string;
}

interface GivesBothAndEither extends GivesNumber, GivesString{
  giveEither(): number | string;
}

function useGivesBoth(instance: GivesBothAndEither){
  instance.giveEither(); // number | string
  instance.giveNumber(); // number
  instance.giveString(); // string
}

🔷인터페이스 병합

두 개의 인터페이스가 동일한 이름으로 동일한 스코프에 선언된 경우, 선언된 모든 필드를 포함하는 더 큰 인터페이스가 코드에 추가된다

interface Merged {
  fromFirst: string;
}

interface Merged {
  fromSecond: number;
}

// 다음과 같음
// interface Merged {
//   fromFirst: string;
//   fromSecond: number;
// }

일반적으로 인터페이스 병합을 자주 사용하지는 않는다.
인터페이스가 여러 곳에 선언되면 코드를 이해하기가 어려워지기 때문이다.

그러나 인터페이스 병합은 외부 패키지 또는 Window 같은 내장된 전역 인터페이스를 보강하는 데 특히 유용하다.

interface Window{
  myEnvironmentVariable: string;
}

window.myEnvironmentVariable; // 타입: string

🔸이름이 충돌되는 멤버

병합된 인터페이스는 타입이 다른 동일한 이름의 속성을 여러 번 선언할 수 없다.
그러나 병합된 인터페이스는 동일한 이름과 다른 시그니처를 가진 메서드는 정의할 수 있다.
이런 경우, 함수 오버로드가 발생한다.

interface MergedMethods {
  different(input: string): string;
}

interface MergedMethods {
  different(input: number): string;
}

[참고] : 러닝 타입스크립트 (한빛 미디어)

profile
프론트에_가까운_풀스택_개발자

0개의 댓글