TS 기본

OkGyoung·2023년 6월 22일
0

2023.11 이전 자료

목록 보기
20/30

TypeScript는 정적 타입 검사기이다.

TypeError

올바르지 않은 타입을 사용할 때 생기는 오류입니다.

// 'message'의 프로퍼티 'toLowerCase'에 접근한 뒤
// 이를 호출합니다
message.toLowerCase();
// 'message'를 호출합니다
message();

위에서 message가 문자면 아래 호출이 오류일것이고 함수라면 위에 toLowerCase가 오류입니다.
이와같이 잘못된 타입에 미리 오류를 호출 해주는 것이 TypeError입니다. 이러한 에러를 미리 정적으로 알려주는 것이 typescript의 큰 역할중 하나입니다. 또한 알려주는 오류를 정리하자면

  1. 오타 (소대문자 포함)
  2. 호출되지 않은 함수
  3. 기본적인 논리 오류(항상 실행되지 않는 if문 같은...)

또 typescript는 코딩을 하면 자동완성으로 코드를 제안해 줍니다.

물론 tsc [파일명] 을 통해서 바로 확인할 수 있습니다.

명시적 타입

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}

위의 코드를 보면 인자 2개에 각각 string, Date라고 표시해줍니다 이는 TypeScript의 명시적타입으로 사전에 해당하는 변수가 어떤 값을 가지는지 알려주어 오류를 최소화합니다.

또 명시적타입을 적는 것을 잊어버려도 똑똑한 TypeScript는 명시적타입을 추론해 알려줍니다.

다운레벨링

또 TypeScript는 ES3라는 아주 구버전의 ECMAScript를 타겟으로 동작하는 것이 기본 동작입니다. 물론 설정을 통해서 상위 ES로 설정할 수 있지만 구버전에서의 작동은 더욱 넓은 생태계에 적용 가능 하도록 만들 수 있습니다.

타입정리

먼저 원시적인 타입을 보겠습니다.

string, number, boolean

배열

그리고 타입을 모든 타입 any가 있습니다.

사용은 변수선언 시 바로 넣어줄 수 있습니다.
물론 넣어주지않아도 밑에 타입은 string으로 추론됩니다.

let myName: string = "Alice";

함수의 경우 아래처럼 반환타입을 명시할 수 있습니다.

function getFavoriteNumber(): number {
  return 26;
}

객체의 경우 아래처럼 타입을 명시할 수 있습니다.

function printCoord(pt: { x: number; y: number }) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

또 "?"통해 옵션으로(있어도되고 없어도되고) 타입을 선언 할 수 있습니다.

function printName(obj: { first: string; last?: string }) {
  // ...
}
// 둘 다 OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

하지만 위에 last는 옵션인지입니다. 즉 함수 속에서 사용할려면 정의 여부를 체크해야합니다.

유니온타입

만약 string 혹은 number이라면 어떻게 하시겠습니까?

function printId(id: number | string) {
  console.log("Your ID is: " + id);
}

그럴경우 위처럼 | 를 통해 2가지타입을 가능하게 할 수 있습니다.

타입만들기

type Point = {
  x: number;
  y: number;
};

위는 타입을 직접 만들어준 형태입니다. 즉 Point타입은 number로된 x,y를 가지는 객체 타입입니다.

type UserInputSanitizedString = string; 이것처럼 별명을 만들어서 사용 가능

또한 타입들은 아래처럼 원하는 모습으로 확장가능합니다. 하지만 보는 것처럼 타입은 한번 생성되면 후에 수정할 수 없습니다.

타입단언

아래의 두코드는 같은 의미입니다. 보는 것처럼 HTMLCanvasElement타입을 가진다고 알려주는 것입니다. 왜냐면 typescript와 다르게 개발자는 정확히 이 코드가 내가 사용할 때 반환하는 값을 알고있기 때문입니다.

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

리터럴타입

아래코드에서 left, right, center은 리터럴타입입니다. 즉 유니온으로 이루어진 3개의 리터럴타입으로 alignment가 가질 수 있는 값을 제한합니다.

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");

null, undefined

만약 strictNullChecks가 체크되어있다면(권장됨) 아래 처럼 undefined를 체크할 수 있습니다.
또 !를 통해 x가 항상 값을 가질 수 있다고 선언할 수 있고 ?를 통해 값이 없을수 있다고 선언할 수도 있습니다.

function liveDangerously(x?: number | undefined) {
  // 오류 없음
  console.log(x!.toFixed());
}

타입가드

만약 특정 변수가 특정 타입으로 들어왔을 때는 어떤식으로 체크 할까요 바로 typeof입니다

if (typeof padding === "number")

typeof는 "string" "number" "bigint" "boolean" "symbol" "undefined" "object" "function"값을 가질 수 있습니다.

boolean

다음은 boolean의 fasle로 여겨지는 값입니다.

0, NaN, "", 0n, null, undefined

부수적으로 만약 2값이 number|string, number|object라면 두 값이 같을 경우는 두 값 모두 number일 때 뿐입니다. 이럴때는 타입이 자동적으로 number로 고정되고 이를"Equality narrowing"이라 합니다. 비슷한 경우로 특정배열의 값에 in을 사용하였다면 in을 통과한 값은 항상 배열속 값중 하나일 것이고 즉 가능한 타입이 배열 속 값으로 정해집니다.

또 x instanceof Date 처럼 특정 인스턴스인지도 확인할 수 있습니다.

하지만 이렇게 선언된 다양한 타입들에 값이 변한다면 어떻게 될까요?
위에서는 let으로 선언된a에 string혹은 number이 들어가도록 했다면 아래에서는 true나 null같은 다른 값을 넣어주면 바로 잘못되었다고 알려줍니다.

never

typescript는 never유형을 사용하여 존재해서는 안 되는 상태를 나타냅니다.

함수타입

아래는 ()는 함수타입을 나타냅니다 즉 someArg를 인수로 boolean을 반환값으로 가지는 함수를 객체의 요소로 가지는 타입입니다.

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

제네릭

아래 코드를 보면Type라는 알수없는 타입이 있습니다. 이는 무었이든 될수있다는 뜻입니다. 만약 string이라면 string[]이고 string값을 반환하는 식이 될것입니다.

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

물론 단일 타입이아닌 여러타입도 가능합니다.

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

하지만 일부 제약이 필요한 경우도 있습니다.
아래코드처럼 아무값이나 가능하지만 적어도 length는 있어서 그것을 비교 할꺼야 하고한다면 extends를 통해 제약을 줄 수 있습니다.

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

좋은 제네릭을 선언하는 방법
단순하게 보면 아래는 같은 코드로 보일 수 있습니다. 둘 다Type를 반환한다고 생각할 수 있지만
실제로 2번째 함수의 경우 반환되는 값은 any입니다

function firstElement1<Type>(arr: Type[]) {
  return arr[0];
}
 
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}
const a = firstElement1([1, 2, 3]);
const b = firstElement2([1, 2, 3]);

또 "fn:(arg: any, index?: number) => void"처럼 callback함수를 타입으로 표현 할수있습니다.

나머지 인수

아래는 획기적인 방법입니다. n은 수의 첫번째를 m은 그외 나머지를 담는 배열입니다.

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

구조분해할당

아래는 구조분해할당한 함수에 각각 타입을 설정해주는 것입니다. 실제로 자주 사용합니다.

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

readonly

아래코드는 무엇을 말할까요 JS 객체는 mutable합니다 즉 외부에서 수정한다면 그 값을 참조하는 모든 곳에서 변해버립니다. 이를 방지하기위해 readonly로 읽기 전용으로 만들 수 있습니다.
물론 readonly를 필요한 수정이 끝난 후 적용하여 안전성을 추가할 수 있습니다

interface SomeType {
  readonly prop: string;
}

색인서명

만약 우리가 사용하는 객체의 속성명을 정확하게 알 수 없다면 어떨까요? 하지만 그 값의 타입을 알수있다면 아래처럼 사용할 수 있습니다. 풀어 설명하자면 [index : number]는 []사이에number이 가능하고 그 반환값은 string이라는 것입니다.

interface StringArray {
  [index: number]: string;
}
 
const myArray: StringArray = getStringArray();
const secondItem = myArray[1];
          
const secondItem: string

하지만 항상 반환값이 1가지 타입은 아닐 수 있습니다.
아래의 코드를 보면 알 수 있듯 배열은 number일수도, string일수도 있습니다. 그래서 반환타입도 string|number로 되어 있구요

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // ok, length is a number
  name: string; // ok, name is a string
}

초과속성확인

아래코드를 확인하면 문제가 없다고 생각하기 쉽상입니다. 하지만 TypeScript는 오류는 보여줍니다 왜일까요 정답은 "let mySquare = createSquare({ colour: "red", width: 100 });"에 있습니다 createSquare넣어주는 객체는 {}인데 이 속에 다른 속성이 없다는 확신을 가질 수 없습니다.(이후에 추가될 수도 있다) 그래서 정확하게 알려줘야합니다.
"let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);"

interface SquareConfig {
  color?: string;
  width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 20,
  };
}
 
let mySquare = createSquare({ colour: "red", width: 100 });

물론 위에 방법으로 해결하는 것도 좋지만 객체에 다른 값이 추가되지 않을 것이라는 확신은 좋지않습니다. 언제 데이터가 변할지 모르니까요 그렇다면 아래처럼 가능성을 열어두고 개발하는 것이 좋을 것입니다.

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

아니면 이후에 확장에 대한 정확한 인터페이스를 만들어 주어도 괜찮습니다.

interface BasicAddress {
  name?: string;
  street: string;
  city: string;
  country: string;
  postalCode: string;
}
 
interface AddressWithUnit extends BasicAddress {
  unit: string;
}

막얀 정말 객체에 대한 아무정보가 없다면 어떻게 할까요? 바로 제네릭입니다.
아래는 Box객체에 대한 제네릭선언입니다. Box는 어떠한 타입이라도 가능합니다. 물론 선언 시 타입을 지정해줘야합니다. "let box: Box"처럼

interface Box<Type> {
  contents: Type;
}

Array

아래모습은 Array제네릭입니다. 또 ReadonlyArray변경하면 안 되는 배열을 설명하는 특수 유형을 선언할 수도 있습니다.

function doSomething(value: Array<string>) {
  // ...
}
 
let myArray: string[] = ["hello", "world"];
 
// either of these work!
doSomething(myArray);
doSomething(new Array("hello", "world"));

튜플

아래는 튜플을 사용한 분할입니다. 배열에요소가 string, number, 나머지 boolean인것을 각각 name, version, ...input넣어 사용할 수 있습니다. API와 통신할 때 아주 유용하게 사용 가능합니다

function readButtonInput(...args: [string, number, ...boolean[]]) {
 const [name, version, ...input] = args;
 // ...
}

keyof

keyof는 객체타입에서 객체의 속성명을 가져와 리터널 유니온을 만듭니다 a|b|c...처럼 .

profile
이유를 생각하는 개발자

0개의 댓글