티스토리 뷰

 

타입스크립트 공부를 다시 시작하며 기본기를 다지기 위해 문서를 통해 다시 공부를 시작하였습니다.

공부는 타입스크립트 문서를 통해서 동일한 순으로 진행하였습니다.

따라서 주관적인 생각이 주입이 되었을 수 있습니다. 정확한 내용은 아래 레퍼런스 문서를 보고 판단하시길 바랍니다.

레퍼런스 : https://www.typescriptlang.org/ko/docs/handbook/2/basic-types.html

레퍼런스 : https://www.typescriptlang.org/ko/docs/handbook/2/everyday-types.html

 

Documentation - Everyday Types

언어의 원시 타입들.

www.typescriptlang.org

 

Documentation - The Basics

TypeScript를 배우는 첫 걸음: 기본 타입.

www.typescriptlang.org

 

 

정적 타입 검사

타입스크립트는 정적인 타입이 선언됨으로써 값들의 형태를 추론할 수 있고, 이 때문에 프로그램이 제대로 동작하지 않을 수 있는 요소들을 알려줍니다.

 

const notFunctionIsString = "this is string!!!!";

notFunctionIsString();
// This expression is not callable. Type 'String' has no call signatures.

 

실행 실패에 예외를 두지 않는 타입스크립트

자바스크립트는 다음과 같은 객체 내부의 프로퍼티에 접근했을때 없는 프로퍼티면 undefined를 반환합니다.

// app.js
const user = {
  name: "Jone",
  id: 10,
};

console.log(user.test);
// undefined

하지만 다음과 같이 타입스크립트에서는 유효한 코드여도 타입스크립트의 정적 타입 시스템에서 오류라고 판단하면 경고를 합니다.

const user = {
  name: "Jone",
  id: 10,
};

console.log(user.test);
// any
// Property 'test' does not exist on type '{ name: string; id: number; }'

 

오타도 잡아주는 타입스크립트

타입스크립트는 다음과 같이 오타를 냈을때 사용하려 했던 구문을 알려주기도 합니다.

const number = 109;

// 다음과 같이 오타를 잡아줍니다.
number.toSrting();
// Property 'toSrting' does not exist on type '109'. Did you mean 'toString'?

number.toString();

 

의도와 다르게 호출되지 않은 함수를 잡아주는 타입스크립트

function getRandomNum() {
  return Math.random < 0.5;
  // Operator '<' cannot be applied to types '() => number' and 'number'.
}

 

위와 같이 타입스크립트는 더 많은 버그를 잡아주어 프로그램에서 오류가 발생되는것을 방지해주는 역할도 합니다.

 

 

모든 타입들

[ 자바스크립트의 원시타입 ]

흔히들 사용되는 원시 타입만 다루겠습니다.

자바스크립트의 원시 타입은 일반적으로 string, number, boolean 값이 있습니다.

해당 타입은 typeof 연산자를 사용하여 타입을 얻을 수 있는 이름과 동일합니다.

 

- string : 문자열

- number : 숫자 ( int와 float과 같은 것은 존재하지 않습니다. )

- boolean : true or false

 

[ any ]

any 타입이 표기되면 해당 값은 임의의 값을 할당할 수 있으며, 암묵적으로 어떠한 타입이 올 수 있다고 명시합니다.

let anyType: any = { any: "any" };

anyType = "any";
anyType = 10;

 

[ 변수에 대한 타입 표기 ]

변수에서는 다음과 같이 변수 이름 다음에 작성합니다.

let string: string = "number";
let number: number = 10;

string = 10;
// Type 'number' is not assignable to type 'string'.
number = "number";
// Type 'string' is not assignable to type 'number'.

하지만 대부분 변수에서는 타입을 작성하지 않아도 됩니다. 타입스크립트는 변수 초기화 단계가 이루어질 때 변수의 타입을 자동으로 추론합니다.

let string = "string";
// type : string
let number = 10;
// type : number

 

[ 함수 ]

매개변수 타입표기

매개변수는 다음과 같이 매개변수 이후에 타입을 표기합니다.

function getName(name: string) {
  // ...
  return `My name is ${name}`;
}

getName("Jone");

getName(10);
// Argument of type 'number' is not assignable to parameter of type 'string'.

- 추후에 다룰 declare을 통한 타입 표기

declare function getName(name: string): void;

getName(10);
// Argument of type 'number' is not assignable to parameter of type 'string'.

 

반환 타입 표기

반환 타입은 매개변수 목록의 뒤에 표기하게 됩니다.

function getMyName(): string {
  return `My name is Jone`;
}

getMyName();
// string

변수에서 타입 표기를 했던것과 마찬가지로 타입스크립트는 return 문을 통해 반환될 값의 타입을 추론하여 일반적으로는 타입을 적지 않아도 됩니다.

function getMyName() {
  return `My name is Jone`;
}

getMyName();
// string

 

익명 함수에서의 문맥적 매개변수 타입 추론

익명 함수에서도 문맥적으로 해당 함수가 호출될지 추론해서 타입이 지정됩니다.

names.forEach(function (s) {
  console.log(s.toUppercase());
  // Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});

 

객체 타입

다음과 같이 매개변수에서의 객체는 객체에 담길 값의 프로퍼티와 타입으로 지정합니다.

function debugPoints(point: { x: number; y: number }) {
  console.log(`points: x:${point.x} y:${point.y}`);
}

debugPoints({ x: 10, y: 10 });

 

옵셔널 프로퍼티

객체에서 선택적으로 값을 받고 싶을때는 프로퍼티 이름 뒤에 ?를 붙이면 됩니다.

function debugPoints(optionalPoints: { x: number; y?: number }) {
  console.log(`points: x:${optionalPoints.x} y:${optionalPoints.y}`);
}

debugPoints({ x: 10 });
// points: x:10 y:undefined


자바스크립트는 존재하지 않는 프로퍼티를 읽을 때 런타임 오류가 발생하지 않고 undefined가 발생합니다.

따라서 옵셔널 프로퍼티를 사용하게되면 프로그램에서 오류가 발생되는 것을 방지하기 위해서는 해당 값이 있는지 여부를 판별하는 코드가 필요합니다. 

function debugName(userName: { first: string; last?: string }) {
  // 해당 프로퍼티의 unefined 여부를 판별합니다.
  if (userName.last !== undefined) {
    console.log(userName.last.toUpperCase());
  }

  // 최신 자바스크립트 스펙의 문법을 사용하면 다음과 같이 ?를 통해 옵셔널 체이닝을 할 수 있습니다.
  console.log(userName.last?.toUpperCase());
}

debugName({ first: "Jone" });

 

[ 유니언 타입 ]

타입스크립트를 통해서 다양한 연산자를 사용해서 새로운 타입을 만들 수 있습니다.

유니언 타입은 서로 다른 두개 이상의 타입을 사용하고자 할 때 사용됩니다.

 

사용 방법은 다음과 같이 타입 사이에 | 연산자 를 사용하여 표기합니다.

이렇게 조합에 사용된 각 타입을 유니언 타입의 멤버 라고 부릅니다.

ex) debugId 의 매개변수 id의 유니언 타입의 멤버는 무엇이 있나요? => number 과 string 있습니다.

function debugId(id: number | string) {
  console.log(`id is:${id}`);
}

debugId(10);

debugId("10");

 

유니언 타입 사용 대처 : typeof

다음과 같이 유니언 타입과 같이 여러 멤버 타입을 받는 함수 내부에서는 다음과 같이 대처를 합니다.

예를 들어 number에서는 toUpperCase와 같은 메서드를 사용할 수 없으므로 각 상황에 맞는 대처를 typeof 를 통해 코드를 작성합니다.

if 문에서 string인 타입만 typeof 로 검사하고 나머지 number의 타입인 경우에서는 else 문으로 처리가 가능한 이유는 id는 numberstring 두 타입만을 받기 때문입니다.

function debugId(id: number | string) {
  if (typeof id === "string") {
    // ....
    // id 타입이 string 인 값을 처리합니다.
  } else {
    // ...
    // number 타입의 값을 처리합니다.
  }
}

debugId(10);

debugId("10");

 

배열에서의 대처 : Array.isArray()

배열은 typeof 를 통해 타입을 걸러내지 않고 Array.isArray()라는 Array의 메서드를 사용합니다.

function debugUsers(id: number | number[]) {
  console.log(`id is:${id}`);
  if (Array.isArray(id)) {
    // ....
    // id 타입이 배열일 때 처리합니다.
  } else {
    // ...
    // number 타입의 값을 처리합니다.
  }
}

debugUsers(10);

 

공통적으로 처리가 가능한 메서드를 사용할 수 있을 때

항상 typeof 나 Array.isArray()를 통해서 타입을 처리하지 않아도 됩니다.

값에서 공통적으로 사용될 수 있는 메서드를 사용할 때 별도의 처리가 필요 없습니다.

function getFirstThree(value: number[] | string) {
  // slice는 배열과 문자열에서 사용할 수 있는 메서드입니다.
  return value.slice(0, 3);
}

getFirstThree([10, 20, 30]);
getFirstThree("123");

 

타입 별칭

지금까지의 타입들을 한 번 이상 재사용을 하거나 명시적으로 이름을 부여해 타입을 사용하고 싶을 때 타입 별칭을 사용합니다. 타입 별칭을 사용하면 새로운 타입이 생성되는 것이 아닌 그저 별칭으로 작동합니다.

// before
function notUseAliasDebugPoint(point: { x: number; y: number }) {
  return console.log(`point : ${(point.x, point.y)}`);
}

notUseAliasDebugPoint({ x: 10, y: 10 });

// after

// 타입 별칭 사용
type Point = {
  x: number;
  y: number;
};

function debugPoint(point: Point) {
  return console.log(`point : ${(point.x, point.y)}`);
}

debugPoint({ x: 10, y: 10 });

 

유니언 타입을 타입 별칭과 함께

type ID = number | string;

 

[ 인터페이스 ]

인터페이스를 선언하면 객체 타입을 만들 수 있습니다.

타입 별칭과 마찬가지로 인터페이스도 별칭에 지나지 않습니다.

interface Point {
  x: number;
  y: number;
}

function debugPoint(point: Point) {
  return console.log(`point : ${(point.x, point.y)}`);
}

debugPoint({ x: 10, y: 10 });

 

 

타입 별칭과 인터페이스의 차이점

두 타입 선언법은 매우 유사하지만 큰 차이점은 타입은 새 프로퍼티를 추가하도록 개방될 수 없고, 인터페이스는 항상 확장 될 수 있다는 점입니다.

 

<확장하기>

- 인터페이스 확장하기

extends 구문을 통해 확장합니다. 2개 이상의 인터페이스를 확장하고 싶다면 extends 이후 , 를 통해 확장합니다.

// 인터페이스 확장하기
interface User {
  name: string;
  age: number;
}

interface SpecialUser extends User {
  authority: number;
}

//... 더 많은 인터페이스를 확장하고 싶다면 extends 구문 다음에 , 를 통해 확장합니다.
interface Somthing {
  somthing: boolean;
}

interface SpecialUser extends User, Somthing {
  authority: number;
}

- 교집합으로 타입 확장하기

& 연산자를 통해 확장하며 2개 이상 확장한다면 확장할 때 &를 연달아 작성해줍니다.

// 교집합으로 타입 확장하기
type User = {
  name: string;
  age: number;
};

type SpecialUser = User & {
  authority: number;
};

//... 더 많은 타입을 확장하기 위해서는 다음과 같이 작성합니다
type Somthing = {
  somthing: boolean;
};

type SpecialUser = User &
  Somthing & {
    authority: number;
  };

 

<필드 추가시>

- 기존의 인터페이스에 필드를 추가하기

// 기존의 인터페이스에 필드를 추가하기
interface User {
  title: string;
}

interface User {
  id: number;
}

const user: User = { title: "test", id: 10 };

 

- 타입으로 선언시 생성 뒤에는 달라질 수 없음

// 기존의 인터페이스에 필드를 추가하기
type User = {
  title: string;
};

type User = {
  id: number;
};

const user: User = { title: "test", id: 10 };
// Error : Identifier 'User' has already been declared

 

 

[ 타입 단언 ]

타입스크립트보다 코드 작성자가 타입에 대한 정보를 더 잘 아는 경우도 있어 명확하게 어떤 값이 반환 될 지 알고 있는 상황에서 타입을 구체적으로 명시를 할 수 있는 타입 단언을 사용할 수 있습니다.

 

- as 타입 단언 (.tsx, .ts 파일에서 사용가능)

 as 연산자를 통해서 반환 될 명확한 타입을 명시해줍니다.

const canvasElement = document.getElementById('canvas_view') as HTMLCanvasElement;

 

- <>꺾쇠를 통한 타입 단언 (.tsx 파일에서 사용불가능, .ts 파일에서 사용가능)

export const canvasElement = <HTMLCanvasElement>document.getElementById("canvas_view");

 

위와 같이 타입 단언은 명확하게 타입을 알 때에 사용해야합니다. 타입스크립트 문서에서 설명하길 타입 단언은 컴파일 시간에 제거되어, 타입 단언에 관련된 검사는 런타임 중에 이루어지지 않아 틀렸더라도 예외가 발생되거나 null 이 생성되지 않습니다.

 

- 불가능한 강제 변환 방지

당연하게도 아래와 같이 초기화 된 값의 타입이 string인데 타입 단언을 통해 number 타입을 지정해주게 되면 경고를 표출합니다.

const x = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake 
// because neither type sufficiently overlaps with the other. 
// If this was intentional, convert the expression to 'unknown' first.

 

- 두 번의 단언으로 타입 변환

아래와 같이 불가피하게 강제 변환이 허용되지 않아 어려움을 겪을 수 있습니다. 그럴 땐 두 번의 단언을 통해 타입을 변환할 수 있습니다.

declare const IDontKnow: any;

type IKnow = { a: 1; b: 2; c: 3; d: 4 };

//...
const whatsThis = IDontKnow as any as IKnow;
// any 또는 unknown 타입으로 바꾸고 변환해줍니다.
// const whatsThis = IDontKnow as unknown as IKnow;

 

[ 리터럴 타입 ]

일반적인 string, number 과 같은 원시적인 타입 이외에 구체적인 값을 지정할 수 있습니다.

 

자바스크립트에서 변수 선언시 var 과 let으로 선언하면 이후에 값을 변경이 가능하고 const로 선언하게 되면 이후 값을 변경 할 수 없는 이 규칙이 타입스크립트가 리터럴 값을 위한 타입을 생성하는 방식에 반영이 됩니다.

 

let으로 선언하고 문자열을 초기값으로 할당하면 선언된 변수는 자동적으로 string 타입이 지정됩니다.

반대로 const 로 선언된 변수는 초기값 자체가 리터럴 타입으로 지정됩니다.

// 선언과 동시에 자동으로 string 타입 지정됨
let canChangeString = "hello";

canChangeString = "wow";
// canChangeString type : string

// 선언과 동시에 cannotchangevalue 문자열 값이 리터럴 타입으로 지정됨
const canNotChangeString = "cannotchangevalue";

canNotChangeString = "wow";
// const canNotChangeString: "cannotchangevalue"

 

유니언 타입과 함께 쓰는 리터럴

리터럴 타입을 하나의 타입만을 지정하는 상수와 같이 사용하게 되면 그다지 쓸모가 없습니다.

여러 타입을 함께 쓸 수 있는 유니언 타입과 함께 사용해서 특정 값을 받을 수 있도록 사용하면 유용합니다.

 

- 그다지 쓸모 없는 하나의 타입만을 허용하는 리터럴 타입

let value: "Hi!!" = "Hi!!";

value = "Hi!!";

value = "Hmm.....";
// Type '"Hmm....."' is not assignable to type '"Hi!!"'.

 

- 유니언 타입과 함께 쓰는 리터럴 타입

리터럴 타입은 다음과 같이 유니언 타입을 지정하면 그 값은 지정된 타입의 값만 사용할 수 있게 됩니다.

아래의 예시에서 소개된 string 이외의 number, 타입 별칭, 인터페이스 등 타입으로 지정할 수 있는 타입을 사용 가능합니다.

function textAlignment(text: String, textAlign: "left" | "right" | "center") {
  // ...
}

textAlignment("this is text", "left");

 

- boolean 타입!!

사실 boolean 타입은 true | false 유니언 타입의 별칭으로써 리터럴 타입입니다.

function boolTest(boolValue: true | false) {
  // boolValue type = boolean
}

// ...
function boolTest(boolValue: boolean) {
  // boolValue type = boolean
}

boolTest(true);

 

리터럴 추론

타입스크립트에서 변수에 객체에 값을 초기화 하여 사용하면 해당 객체의 프로퍼티의 값은 언제나 바뀔 수 있다고 가정합니다.

const obj = { value: 0 };

// 프로퍼티의 값은 언제나 바뀔 수 있다고 가정하여 타입스크립트에서 오류라고 판단하지 않습니다.
// 여기서 프로퍼티 value의 타입은 number로 지정됩니다.
obj.value = 1;

위와 같은 상황은 문자열에서도 이루어집니다.

 

아래에서 request 과정에서 사용되는 메서드 GET, POST 와 같은 유니언 타입과 함께 리터럴 타입으로 지정된 매개변수 값이 있고 객체를 통해 값을 인자로 넘기면 같은 문자열이여도 타입스크립트는 오류라고 판단합니다.

const requestUser = {
  url: "https://getuser",
  method: "GET",
};

function getUser(url: string, method: "GET" | "POST" | "PUT" | "DELETE") {
  // ....
}

getUser(requestUser.url, requestUser.method);
// Argument of type 'string' is not assignable to parameter of type 
// '"GET" | "POST" | "PUT" | "DELETE"'.

 

위와 같은 상황을 해결할 수 있는 방법은 두 가지가 있습니다.

 

1. 타입 단언을 통해 타입을 지정해주기

const requestUser = {
  url: "https://getuser",
  method: "GET" as "GET",
};

function getUser(url: string, method: "GET" | "POST" | "PUT" | "DELETE") {
  // ....
}

getUser(requestUser.url, requestUser.method);

 

const requestUser = {
  url: "https://getuser",
  method: "GET",
};

function getUser(url: string, method: "GET" | "POST" | "PUT" | "DELETE") {
  // ....
}

getUser(requestUser.url, requestUser.method as "GET");

 

2. 타입 단언에서 const 사용하기

타입 단언에서 as const 라는 구문을 사용할 수 있는 이는 const 로 선언한 변수와 유사하게 동작하고 리터럴 타입의 값이 되도록 보장합니다.

const requestUser = {
  url: "https://getuser",
  method: "GET",
} as const;

function getUser(url: string, method: "GET" | "POST" | "PUT" | "DELETE") {
  // ....
}

getUser(requestUser.url, requestUser.method);

 

[ null 과 undefined ]

자바스크립트에서 초기화 되지 않은 빈 값을 의미하는 nullundefined 은 타입스크립트에서도 동일한 이름의 타입이 있습니다. 각 타입의 동작 방식은 strictNullChecks 옵션에 따라서 달라집니다.

 

* strictNullChecks 옵션은 일반적으로 tsconfig.json 파일에서 설정하면 됩니다.

// tsconfig.json
{
  "compilerOptions": {
	// ...
    "strictNullChecks": false // or true
  },
}

 

strictNullChecks : false (또는 설정되지 않았을 때)

해당 옵션이 설정되지 않았다면 null 또는 undefined 값이 할당이 되더라도 접근이 가능합니다.

따라서 예기치 못한 상황에서 제대로 null 과 undefined 를 체크하지 못해서 프로그램에서 오류가 발생될 수 있습니다.

타입스크립트 문서에서는 별다른 이유가 없다면 strictNullChecks 옵션을 설정하는 것을 권장하고 있습니다.

// strictNullChecks:false 또는 설정 안했을 때
function testFunction(value: string | undefined) {
  if (value === undefined) {
    console.log("undefined");
  } else {
    console.log(value.toUpperCase());
  }
}

// 오류가 발생하지 않습니다.
testFunction(null);

 

strictNullChecks : true (설정되었을 때)

다음과 같이 옵션을 설정하면 지정되지 않은 null 타입에서 오류가 발생합니다. 따라서 예기치 못하게 전달되는 인자의 값이 null 일때 별도 처리를 할 수 있습니다.

// strictNullChecks:true
function testFunction(value: string | undefined) {
  if (value === undefined) {
    console.log("undefined");
  } else {
    console.log(value.toUpperCase());
  }
}

testFunction(null);
// Argument of type 'null' is not assignable to parameter of type 'string | undefined'.

다음은 null 의 경우 별도 처리 방법입니다.

function testFunction(value: string | undefined | null) {
  if (value === undefined) {
    console.log("undefined");
  } else if (value === null) {
    console.log("null");
  } else {
    console.log(value.toUpperCase());
  }
}

testFunction(null);

 

null 아님 단언 연산자 (접미사 !)

null 또는 undefined 가 아니라고 타입을 단언해주는 연산자가 있습니다. null 과 undefined 가 절대적으로 아닌 표현식 뒤에 !를 붙이면 됩니다.

function thisNeverNullOrUndefined(value?: number | undefined) {
  console.log(value!.toFixed());
}

위의 코드에서 undefined 값으로 인자를 넘기면 런타임에서 오류가 발생하게 됩니다. 따라서 반드시 확실히 null 또는 undefined 가 아닐 때만 사용해야 합니다.

function thisNeverNullOrUndefined(value?: number | undefined) {
  console.log(value!.toFixed());
}

thisNeverNullOrUndefined(undefined);
// intro.tsx:6 Uncaught TypeError: Cannot read properties of undefined (reading 'toFixed') at thisNeverNullOrUndefined

 

댓글
최근에 올라온 글