자바스크립트의 역사를 살펴보며 타입스크립트의 탄생 기원을 알아보고, 타입스크립트는 자바스크립트에 무엇을 추가했는지, 타입 시스템은 어떻게 작동하는지 살펴보자.
타입스크립트의 강력한 핵심 개념인 유니언과 리터럴을 소개하고 복잡한 객체 형태를 설명하는 방법과 타입스크립트가 객체의 할당 가능성을 확인하는 방법을 소개한다.
자바스크립트는 10일만에 만들어진 언어이다.
결점이 있을 수 밖에 없고, 당시 개발자들은 그런 자바스크립트를 조롱했다.
그러나 자바스크립트 운영위원회인 TC39는 2015년 이후로 매년 새로운 버전의 ECMAScript를 발표하며 자바스크립트를 발전시켰고, 현재 자바스크립트는 브라우저, 임베디드, 서버 런타임을 포함한 다양한 환경에서 사용된다.
function paintPainting(painter, painting){
return painter.prepare().paint(painting, painter.ownMaterials).finish();
}
paintPainting
함수를 호출하는 방법에 대해 너무 막연하다.
매개변수인 painting
이 문자열이라고 추측해서 운좋게 맞아 떨어지더라도 나중에 코드를 변경하면 타입이 달라져서 가정이 무효가 된다.
다른 언어는 컴파일러가 충돌할 수 있다고 판단하면 코드실행을 거부할 수 있다.
하지만 자바스크립트처럼 충돌 가능성을 먼저확인하지 않는 동적 타입언어는 그렇지가 않다.
자바스크립트 사양에는 함수의 매개변수, 함수 반환, 변수 또는 다른 구성 요소의 의미를 설명하는 표준화된 내용이 없다.
그래서 자바스크립트 개발자들은 JSDoc라는 것을 만들었다.
/**
* Performs a painter painting a particular painting.
*
* @param {Painting} painter
* @param {string} painting
* @returns {boolean} Whether the painter painted the painting.
*/
function paintPainting(painter, painting){
return painter.prepare().paint(painting, painter.ownMaterials).finish();
}
그렇지만 JSDoc는 다음의 문제점들을 가지고 있다.
자바스크립트는 타입을 식별하는 내장된 방법이 제공하지 않는다.
따라서 C#이나 자바와 같은 타입이 지정된 언어에서 클래스 멤버 이름을 변경하거나 인수의 타입이 선언된 곳으로 바로 이동하는 기능이 개발자 도구에 없다.
VSCode와 같은 IDE에보면 기능이 있다고 반박할 것이다.
이 기능은 내부적으로 타입스크립트로 만든것이다.
타입스크립트는 다음 4가지로 설명된다.
다음 코드를 살펴보자.
위의 코드는 자바스크립트 코드이다.
실행하기 전에는 오류가 존재하는지 알려주지 않는다.
타입스크립트의 언어 서비스가 실행되어 오류에 빨간 물결선이 표시된다.
이렇듯 타입스크립트는 오류를 미리 알려준다.
타입스크립트를 사용하면 매개변수와 변수에 제공되는 값의 타입을 지정할 수 있다.
코드를 지정한 방법으로만 사용하도록 제한한다면, 타입스크립트는 코드를 변경하더라도 멈추지 않는다는 확신을 줄 수 있다.
타입을 문서화하기 위한 타입스크립트 구문을 아직 배우지 않았지만, 다음 코드를 통해 문서화하는 타입스크립트의 정밀함을 확인할 수 있다.
interface Painter {
finish(): boolean;
ownMaterials: Material[];
paint(painting: string, materials: Material[]): boolean;
}
function paintPainting(painter: Painter, painting: string): boolean {/* */}
Painter
에 적어도 세 가지 속성이 있고, 그 중 두 가지는 메서드라는 것을 알아볼 수 있다.
VSCode는 타입스크립트로 문자열 같은 빌트인 객체를 작성할 때 '자동 완성'을 제안한다는 것을 이미 알고 있을 것이다.
뿐만 아니라 이미 작성된 코드에 대해서도 유용한 제안을 제공한다.
위의 Painter
객체를 예로 들어보자.
painter
를 입력하면 타입스크립트는 painter
의 타입이 Painter
이고 Painter
타입은 세 가지의 멤버를 갖는다는 것을 확인할 수 있다.
타입스크립트 컴파일러에 타입스크립트 구문을 입력하면 타입을 검사한 후 작성된 코드에 해당하는 자바스크립트를 내보낸다.
최신 자바스크립트 구문이나 이전 ECMA스크립트에 상응하는 코드로 컴파일할 수도 있다.
타입스크립트 자체 컴파일러 대신 "바벨"과 같은 전용 변환기를 사용하는 프로젝트들이 많다.
Node.js가 설치되어 있어야 한다.
타입스크립트 최신 버전을 전역으로 설치하려면 다음과 같이 입력한다.
npm i -g typescript
버전확인을 통해 설치가 정상적으로 완료되었는지 확인하자.
tsc --version
아무 곳에나 폴더를 만들고 새 tsconfig.json 구성 파일을 생성한다.
tsc --init
tsconfig.json 파일은 타입스크립트가 코드를 분석할 때 사용하는 설정을 선언한다.
index.ts 파일에 적당한 타입스크립트 코드를 입력한다.
이 파일을 자바스크립트로 컴파일하려면 다음과 같이 입력한다.
tsc index.ts
tsconfig.json 파일을 생성하면 편집기가 해당 폴더를 타입스크립트 프로젝트로 인식한다.
VSCode에서 폴더를 열면 타입스크립트 코드를 분석하는 데 사용하는 설정은 해당 폴더의 tsconfig.json을 따르게 된다.
타입스크립트를 쓰려면 특정 클래스나 패턴을 사용해야 한다는 오해이다.
타입스크립트는 타입 안정성 강화를 제외하고는 어떤 것도 강요하지 않는다.
자바스크립트에서 사용했던 아키텍쳐 패턴 중 무엇이든 사용해서 코드를 작성할 수 있다.
타입스크립트는 클래스나 함수 사용 여부와 같은 코드 스타일 의견을 강요하지 않는다.
타입스크립트와 자바스크립트 간에 충돌이 일어날 것이라는 오해이다.
타입스크립트의 설계 목표는 다음과 같이 명시되어 있다.
이 주장은 부정확하고 오해의 소지가 있다.
타입스크립트가 개발자 코드에 직접적으로 수정을 가하는 작업은 인터넷 익스플로러11과 같이 오래된 런타임 환경을 지원하기 위해 이전 버전의 자바스크립트로 코드를 컴파일하도록 요청하는 경우이다.
하지만 이런 작업 마저도 대다수는 타입스크립트의 컴파일러를 사용하지 않고 트랜스파일을 위한 별도의 도구를 사용한다.
타입스크립트는 오로지 타입 검사용으로만 사용한다.
그렇지만 타입스크립트는 자바스크립트보다 빌드하는 데 시간이 조금 더 걸리기는 한다.
타입스크립트는 런타임환경에서 실행되기 전에 자바스크립트로 컴파일되어야 한다.
빌드 파이프라인에 컴파일 과정이 포함되어 있기때문에 시간이 조금 더 걸린다.
웹의 발전은 끝나지 않았고 타입스크립트도 마찬가지다.
개발자들은 타입스크립트에 버그 수정과 기능 추가를 지속적으로 요청하고 있다.
타입 검사기를 통해 코드를 살펴보고, 코드가 작동하는 방식을 이해하고, 오류가 있는 부분을 알려주는 타입 검사기의 역할까지 간략하게 알아보았다.
타입 검사기가 실제로는 어떻게 작동할까?
타입스크립트의 가장 기본적인 타입은 자바스크립트의 일곱 가지 기본 원시 타입과 같다.
타입스크립트는 초기값을 계산하고 그 값을 갖는 변수의 타입을 유추할 수 있다.
타입스크립트에서는 타입을 표시할 때 일반적으로 소문자로 표시한다.
Boolean 이나 Number와 같은 앞글자만 대문자인 타입명과 구분하기 위해서이다.
이들은 원시값을 감싸는 래퍼 객체이다.
기본적으로 타입스크립트의 타입 시스템은 다음과 같이 작동한다.
let firstName = "Whitney";
firstName.length();
// Error: This expression is not callable.
// Type: 'Number' has no call signatures
firstName
이라는 변수를 이해한다."Whitney"
이므로 firstName
이 string
타입이라고 결론짓는다.firstName
의 length
멤버를 함수처럼 호출하는 코드를 확인한다.string
의 length
멤버는 함수가 아닌 숫자라는 오류를 표시한다.타입스크립트가 코드로 이해할 수 없는 잘못된 구문을 감지할 때 발생한다.
이는 타입스크립트가 자바스크립트 파일을 올바르게 생성할 수 없도록 차단한다.
물론 설정에 따라 차단하지 않을 수도 있지만, 예상과는 상당히 다른 결과가 나올 수 있다.
let let wait;
// Error: ',' expected.
타입 검사기가 프로그램의 타입에서 오류를 감지했을 때 발생한다.
이 오류는 구문 오류와 달리 자바스크립트로의 변환을 차단하지 않는다.
단, 생성된 자바스크립트 코드가 원하는 대로 실행되지 않을 가능성이 있다는 신호를 타입 오류로 알려준다.
타입스크립트는 변수의 초기값을 읽고 해당 변수가 허용되는 타입을 결정한다.
나중에 해당 변수에 새로운 값이 할당되면, 새롭게 할당된 값의 타입이 변수의 타입과 동일한지 확인한다.
타입스크립트에서 함수 호출이나 변수에 값을 제공할 수 있는지 여부를 확인하는 것을 "할당 가능성 ( assignability ) 라고 한다.
"Type ... is not assignable to type ..." 형태의 오류는 할당 가능성 오류이다.
초기값을 할당하지 않는 변수를 선언할 때가 있다.
타입스크립트는 나중에 사용할 변수의 초기 타입을 파악하지 않는다.
기본적으로 이런 변수는 암묵적인 any 타입으로 간주한다.
초기 타입을 유추할 수 없는 변수는 "진화하는 any" 라고 부른다.
특정 타입을 강제하는 대신 새로운 값이 할당될 때마다 변수 타입에 대한 이해를 발전시킨다.
let rocker; // 타입: any
rocker = "Joan Jett" // 타입: string
rocker.toUpperCase() // ok
rocker = 19.58; // 타입: number
rocker.toPrecision(1) // ok
rocker.toUpperCase() // Error
// 'toUpperCase()' does not exist on type 'number'
일반적으로 any 타입을 사용해 any 타입이 진화하는 것을 허용하게 되면 타입스크립트의 타입 검사를 부분적으로 쓸모없게 만든다.
타입스크립트는 초기값을 할당하지 않고도 변수의 타입을 선언할 수 이는 구문인 "타입 애너테이션 ( type annotation ) 을 제공한다.
let rocker: string;
rocker = "Joan Jett";
이러한 타입 애너테이션은 타입스크립트 구문으로 자바스크립트에는 영향을 주지 않는다.
타입 애너테이션은 타입스크립트가 자체적으로 수집할 수 없는 정보를 타입스크립트에 제공할 수 있다.
즉시 유추할 수 있는 변수에도 타입 애너테이션을 사용할 수 있다.
다음 코드에서 string
타입 애너테이션은 중복이다.
타입스크립트가 이미 firstName
이 string
타입임을 유추할 수 있기 때문이다.
let firstName: string = "Tina";
바로바로 유추할 수 있는 곳엔 타입 애너테이션이 불필요하다.
코드를 명확하게 문서화하거나 실수로 변수 타입이 변경되지 않도록 타입스크립트를 보호하기 위해 명시적으로 사용하는 것이 경우에 따라서는 유용할 수 있으나 무분별하게 사용하는 것은 불필요한 일이다.
보통 많은 개발자들은 아무것도 변하지 않는 변수에는 타입 애너테이션을 추가하지 않는것을 선호한다.
타입스크립트는 변수에 할당된 값이 원래 타입과 일치하는지 확인하는 것 이상을 수행한다.
타입스크립트는 객체에 어떤 멤버들이 존재하는지 알고 있다.
만약 개발자가 코드에서 변수의 속성에 접근하려고 한다면 타입스크립트는 접근하려는 속성이 해당 변수의 타입에 존재하는지 확인한다.
let rapper = "Queen Latifah";
rapper.length; // ok
rapper.push() // Error: Property 'push' does not exist on type 'string'.
타입은 더 복잡한 형태, 특히 객체일 수도 있다.
let p = {
title: "title",
}
p = {
title: "title",
content: "content",
}
// Error: Type '{ title: string; content: string; }' is not assignable to type '{ title: string; }'.
// Object literal may only specify known properties, and 'content' does not exist in type '{ title: string; }'.
타입스크립트는 객체의 형태에 대한 이해를 바탕으로 할당 가능성뿐만 아니라 객체 사용과 관련된 문제도 알려준다.
타입스크립트는 CommonJS와 같은 이전 모듈을 사용해서 작성된 타입스크립트 파일의 import, export 형태는 인식하지 못한다.
타입스크립트는 일반적으로 CommomJS 스타일의 require 함수에서 반환된 값을 any 타입으로 인식한다.
타입스크립트가 해당 값을 바탕으로 추론을 수행하는 두 가지 핵심 개념
let mathematician = Math.random() > 0.5 ? undefined : "Mark Goldberg";
mathematician
의 타입은 undefined
혹은 string
이다.
"이거" 혹은 "저거" 와 같은 타입을 유니언이라고 한다.
mathematician
변수에 마우스를 가져다 대보자.
string | undefined
타입으로 간주된다.
위의 예제처럼 여러개의 정해진 타입 중 어떤 값이 할당될 지 모를 때 유니언 타입을 사용한다.
또는 변수의 초기값이 있더라도 변수에 대한 명시적 타입 애너테이션을 제공하는 것이 유용할 때 유니언 타입을 사용한다.
let thinker: string | null = null;
if(Math.random() > 0.5) {
thinker = "Susanne Langer"; // ok
}
thinker
의 초기값은 null
이지만 유니언 타입을 선언함으로써 잠재적으로 string
이 될 수 있음을 미리 알려준다.
유니언 타입의 순서는 중요하지 않다.
string | null
이든null | string
이든 똑같다.
값이 유니언 타입일 때 타입스크립트는 유니언으로 선언한 모든 타입에 존재하는 멤버 속성에만 접근할 수 있다.
let physicist = Math.random() > 0.5 ? "Marie Curie" : 84;
physicist.toString(); // string, number 둘 다 있는 메서드
physicist.toUpperCase(); // string 메서드
// Error: Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
physicist.toFixed(); // number 메서드
// Error: Property 'toFixed' does not exist on type 'string | number'.
// Property 'toFixed' does not exist on type 'string'.
모든 유니언 타입에 존재하지 않는 멤버에 대한 접근을 제한하는 것은 안전 조치에 해당한다.
유니언 타입으로 정의된 여러 타입 중 하나의 타입으로 된 값의 멤버를 사용하려면 내로잉을 거쳐 하나의 타입이라는 것을 타입스크립트에게 알려줘야 한다.
내로잉은 값이 정의, 선언 혹은 이전에 유추된 것보다 더 구체적인 타입임을 코드에서 유추하는 것이다.
타입을 좁히는 데 사용할 수 있는 논리적 검사를 타입 가드 ( type guard ) 라고 한다.
변수에 값을 직접 할당하면 타입스크립트는 변수의 타입을 할당된 값의 타입으로 좁힌다.
let admiral: number | string;
admiral = "Grace Hopper";
admiral.toUpperCase();
admiral.toFixed();
// Error: Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?
변수에 유니언 타입 애너테이션이 명시되고 초기값이 주어질 때 값 할당 내로잉이 작동한다.
admiral
은 number | string
타입으로 선언되었지만 초기값으로 문자열이 할당되었기 때문에 타입스크립트는 즉시 string
타입으로 바로 좁혀졌다는 것을 알고 있다.
일반적으로 타입스크립트에서는 변수가 알려진 값과 같은지 확인하는 if문을 통해 변수의 타입을 좁힐 수 있다.
let scientist = Math.random() > 0.5 ? "Rosalind Franklin" : 51;
if (scientist === "Rosalind Franklin") {
scientist.toUpperCase(); // ok
}
scientist.toUpperCase();
// Error: Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
주의할 점은 위의 코드에 else문을 추가하면 string | number
유니언 타입에서 string
타입을 걸러낸 나머지 number
타입만 남아있기에 number
타입의 맴버를 사용할 수 있다고 생각된다는 점이다.
let scientist = Math.random() > 0.5 ? "Rosalind Franklin" : 51;
if (scientist === "Rosalind Franklin") {
scientist.toUpperCase(); // ok
} else {
scientist.toFixed();
// Error: Property 'toFixed' does not exist on type 'string | number'.
// Property 'toFixed' does not exist on type 'string'.
}
else로 걸러진 부분에 마우스를 가져다대면 scientist
변수의 타입은 string | number
이다.
이는 "Rosalind Franklin"이 string
타입의 값일 뿐 string
타입 자체가 아니기 때문이다.
뒤에서 배울 내용이지만 만약 유니언 타입이 Rosalind Franklin | number
라는 문자열 리터럴로 이루어져 있다면 오류가 발생하지 않는다.
let scientist: "Rosalind Franklin" | number =
Math.random() > 0.5 ? "Rosalind Franklin" : 51;
if (scientist === "Rosalind Franklin") {
scientist.toUpperCase(); // ok
} else {
scientist.toFixed();
}
타입스크립트는 직접 값을 확인해 타입을 좁히기도 하지만, typeof
연산자를 통해 타입을 확인할 수도 있다.
let scientist = Math.random() > 0.5 ? "Rosalind Franklin" : 51;
if (typeof scientist === "string") {
scientist.toUpperCase(); // ok
} else {
scientist.toFixed();
}
위의 코드는 string
타입 자체를 걸렀기 때문에 오류가 없다.
이러한 코드는 삼항 연산자를 이용해 다시 작성할 수 있다.
let scientist = Math.random() > 0.5 ? "Rosalind Franklin" : 51;
typeof scientist === "string" ? scientist.toUpperCase() : scientist.toFixed();
typeof
를 사용할 때의 주의할 점은 typeof
이 null
타입을 반환하지 않는다는 점이다.
이는 오래되고 유명한(?) 자바스크립트 오류이다.
console.log(typeof null); // "object"
console.log(typeof {}); // "object"
null
체크는 값으로 체크할 수 있다.
console.log(null === null); // true
자바스크립트에서 false
, 0
, -0
, 0n
, ""
, null
, undefined
, NaN
처럼 falsy로 정의된 값을 제외한 모든 값은 모두 참이다.
타입스크립트는 잠재적인 값 중 truthy로 확인된 일부에 한해서만 변수의 타입을 좁힐 수 있다.
다음 코드에서 geneticist
는 string | undefined
유니언 타입이며 undefined
는 항상 falsy이므로 타입스크립트는 if문의 코드 블록에서는 geneticist
가 string
타입이 되어야 한다고 추론할 수 있다.
let geneticist = Math.random() > 0.5 ? "Barbara McClintock" : undefined;
if (geneticist) {
geneticist.toUpperCase(); // ok: string
}
geneticist.toUpperCase();
// Error: 'geneticist' is possibly 'undefined'.
논리 연산자인 &&
와 ?.
를 사용하여 참 여부를 검사할 수도 있다.
let geneticist = Math.random() > 0.5 ? "임의의 문자열" : undefined;
geneticist && geneticist.toUpperCase();
geneticist?.toUpperCase();
다만, 참일 때 string
으로 좁힐 수는 있지만, 빈 문자열은 falsy값이므로 포함되지 않는다는 것을 유념해야 한다.
즉, null
, undefined
타입과 나머지 타입들을 구분 할 때 truthy / falsy 로 구분해서는 안된다.
리터럴 타입은 좀 더 구체적인 버전의 원시 타입이다.
const philosopher = "Hypatia";`
위의 philosopher
타입은 string
타입이다.
하지만 단지 string
타입이 아닌 "Hypatia" 라는 특별한 값이다.
이것이 바로 리터럴 타입의 개념이다.
원시 타입 값 중 임의의 값이 아닌 특정 원시값으로 알려진 타입이 리터럴 타입이다.
만약 변수를 const로 선언하고 직접 리터럴 값을 할당하면 타입스크립트는 해당 변수를 할당된 리터럴 값으로 유추한다.
위에서 잠깐 언급했지만, 리터럴과 원시 타입을 섞어서 유니언 타입 애너테이션을 만들 수 있다.
let lifespan: number | "ongoing" | "uncertain";
lifespan = 89;
lifespan = "ongoing";
lifespan = true;
// Error: Type 'true' is not assignable to type 'number | "ongoing" | "uncertain"'.
"Ada" 와 "Byron" 처럼 동일한 원시 타입일지라도 서로 다른 리터럴 타입은 서로 할당할 수 없다.
그러나 리터럴 타입은 그 값에 해당하는 원시 타입에 할당할 수 있다.
let someString: string;
let specifically: "Ada";
specifically = "Ada";
specifically = "Byron";
// Error: Type '"Byron"' is not assignable to type '"Ada"'.
someString = specifically; // ok
타입스크립트에서 리터럴로 좁혀진 유니언 타입은 strict null checking ( 엄격한 null 체크 ) 와 함께 사용할 때 매우 유용하다.
타입스크립트 컴파일러는 실행 방식을 변경할 수 있는 다양한 옵션을 제공한다.
가장 유용한 옵션 중 하나인strictNullChecks
는 엄격한 null 검사를 활성화할지 여부를 결정한다.
타입스크립트의 모범 사례는 일반적으로 엄격한 null 검사를 활성화하는 것이다.
strict null checking이 활성화되면 타입스크립트는 기본적으로 변수에 null 또는 undefined를 할당할 수 없게 된다.
하지만 리터럴로 좁혀진 유니언 타입을 사용하면 특정 값을 명시적으로 지정하여 변수에 할당할 수 있다.
이를 통해 해당 변수에는 다른 값이 할당될 가능성이 없어지므로, 변수의 유효성을 높이고 코드의 안정성을 향상시킬 수 있다.
strictNullChecks
옵션을 false
로 설정해보자.
let result: "success" | "failure" | "error";
result = "success"; // ok
result = "failure"; // ok
result = "error"; // ok
result = null; // ok
result = undefined; // ok
result = "invalid";
// Error: Type '"invalid"' is not assignable to type '"success" | "failure" | "error"'.
result 변수에 null
과 undefined
값이 할당 될 때 에러를 발생시키지 않는다.
이는 잠재적인 버그가 발생될 수 있는 코드이다.
C++이나 자바같은 언어는 위와 같이 엄격한 null 검사를 하지 않는 언어이기 때문에 위와 같은 버그가 발생될 것을 대비해서 코드를 짜야 한다.
하지만 타입스크립트는 엄격한 null 검사를 하는 언어이기 때문에 위와 같은 잠재적인 버그를 사전에 알아차릴 수 있다.
strictNullChecks
옵션을 true
로 설정해보자.
let result: "success" | "failure" | "error";
result = "success"; // 유효한 값
result = "failure"; // 유효한 값
result = "error"; // 유효한 값
result = null;
// Error: Type 'null' is not assignable to type '"success" | "failure" | "error"'.
result = undefined;
// Error: Type 'undefined' is not assignable to type '"success" | "failure" | "error"'.
result = "invalid";
// Error: Type '"invalid"' is not assignable to type '"success" | "failure" | "error"'.
null
또는 undefined
값을 할당하려 할 때 타입 에러가 발생되어 개발자로 하여금 선제적으로 버그를 없애도록 한다.
자바스크립트에서 초기값이 없는 변수는 기본적으로 undefined
가 된다.
만약 undefined
를 포함하지 않는 타입으로 변수를 선언한 다음, 값을 할당하기 전에 사용하려고 시도하면 어떻게 될까?
let mathematician: string;
mathematician?.length;
// Error: Variable 'mathematician' is used before being assigned.
위의 코드는 런타임 오류가 없는 코드이다.
즉, 자바스크립트상에서는 오류가 없다.
mathematician
변수에는 undefined
값이 할당되고 mathematician?.length
도 undefined
값이 된다.
하지만 타입스크립트는 값이 할당될 때까지 변수가 undefined
임을 이해할 만큼 똑똑하기에 해당 변수를 사용하려고 시도하면 유효한 타입이 아니기 때문에 오류를 발생시킨다.
변수 타입에 undefined
가 포함되어 있는 경우에는 오류를 발생시키지 않는다.
유효한 타입이기 때문이다.
let mathematician: string | undefined;
mathematician?.length;
타입스크립트에는 재사용하는 타입에 더 쉬운 이름을 할당하는 타입 별칭 ( type alias ) 이 있다.
타입 별칭은 다음과 같은 형태를 갖는다.
편의상 타입 별칭은 파스칼 케이스로 이름을 지정한다.
type MyName = ...;
타입별칭은 "복붙"처럼 작동한다.
다음 예제 코드를 보자.
type RawData = boolean | number | string | null | undefined;
let rawDataFirst: RawData;
let rawDataSecond: RawData;
타입 별칭은 타입이 복잡해질 때마다 사용할 수 있는 편리한 기능이다.
타입 별칭은 타입 애너테이션처럼 자바스크립트로 컴파일되지 않는다.
즉, 런타임 코드에서는 참조할 수 없다.
타입 스크립트는 런타임에 존재하지 않는 항목에 접근하려고 하면 타입 오류로 알려준다.
type SomeType = string | undefined;
console.log(SomeType);
// Error: 'SomeType' only refers to a type, but is being used as a value here.
타입 별칭은 다른 타입 별칭을 참조할 수 있다.
type Id = number | string;
type IdMaybe = Id | undefined | null;
사용 순서대로 타입 별칭을 선언할 필요는 없다.
참조할 타입 별칭을 먼저 사용하고 나중에 정의해도 된다.
위의 코드의 순서를 바꾸어도 된다.
type IdMaybe = Id | undefined | null;
type Id = number | string;
복잡한 객체 형태를 설명하는 방법과 타입스크립트가 객체의 할당 가능성을 확인하는 방법에 대해 알아보자.
{...} 구문을 사용해서 객체 리터럴을 생성하면, 타입스크립트는 해당 속성을 기반으로 새로운 객체 타입 ( 또는 타입 형태 ) 를 고려한다.
해당 객체 타입은 객체의 값과 동일한 속성명과 원시 타입을 갖는다.
const poet = {
born: 1935,
name: "mary Oliver",
};
기존 객체에서 직접 타입을 유추하는 방법도 좋지만, 명시적으로 선언하는 방법도 필요하다.
let poetLater: { born: number; name: string };
poetLater = {
born: 1935,
name: "Mary Oliver",
};
또는 다음과 같이 한 번에 작성할 수도 있다.
let poetLater: { born: number; name: string } = {
born: 1935,
name: "mary Oliver",
};
{ born: number; name: string }
과 같은 객체 타입을 매번 작성하는 것은 매우 귀찮다.
각 객체 타입에 타입 별칭을 할당해 사용하는 방법이 더 일반적이다.
type Poet = { born: number; name: string };
let poetLater: Poet;
poetLater = {
born: 1935,
name: "Mary Oliver",
};
사실 대부분의 타입스크립트 프로젝트는 객체 타입을 설명할 때 별칭 객체 타입 보다 인터페이스 키워드를 사용하는 것을 선호한다.
인터페이스는 뒤에서 알아볼 예정이다.
지금은 별칭 객체 타입과 거의 동일하다고 보면 된다.
타입스크립트의 타입 시스템은 구조적으로 타입화 ( structurally typed )되어 있다.
구조적 타이핑은 타입이 구조 ( 프로퍼티의 집합과 타입 ) 에 따라 결정된다는 의미다.
이는 타입스크립트에서 두 객체가 동일한 타입으로 간주되는 조건이 해당 객체들의 구조적 호환성에 의해 결정된다는 것을 의미한다.
구조적 타이핑은 타입 호환성을 판단할 때 객체의 구조가 중요하며, 타입 주석이나 명시적인 타입 선언 없이도 타입 호환성을 판단할 수 있다.
예를 들어, 다음과 같은 코드를 보자.
interface Point {
x: number;
y: number;
}
let point: Point = { x: 0, y: 0 };
let point2 = { x: 0, y: 0 };
point2 = point; // 구조적으로 호환됨
위의 예제에서 point
변수와 point2
변수는 모두 x
와 y
라는 동일한 프로퍼티를 가지고 있다.
타입스크립트는 이 두 변수가 구조적으로 호환되는 것으로 간주하며, 서로 할당이 가능하다고 판단한다.
구조적 타이핑은 덕 타이핑 ( duck typing )과는 조금 다르다.
덕 타이핑은 "오리처럼 걷고, 오리처럼 꽥꽥거리면 그건 오리다"라는 유명한 말에서 영감을 받은 개념이다.
이는 객체의 구조와 동작에 의해 결정된다는 개념을 나타낸다.
덕 타이핑은 구조적 타이핑의 한 형태로 볼 수도 있다.
덕 타이핑은 객체가 특정한 인터페이스나 클래스를 상속받아야 하는 것이 아니라, 해당 객체가 필요한 메서드나 프로퍼티를 가지고 있는지를 확인하여 타입 호환성을 결정한다.
즉, 객체가 어떤 타입인지가 중요한 것이 아니라, 객체의 동작이 어떤지가 중요하다.
이는 객체가 특정 인터페이스를 명시적으로 구현하지 않아도 해당 인터페이스를 사용하는 코드에서 사용될 수 있게 한다.
구조적 타이핑과 덕 타이핑의 차이점은 주로 개념적인 차이에 있다.
구조적 타이핑은 객체의 구조(프로퍼티의 집합과 타입)에 의해 타입 호환성을 결정하는 반면, 덕 타이핑은 객체의 동작에 의해 타입 호환성을 결정한다.
구조적 타이핑은 객체의 구조가 동일하면 호환성이 있다고 간주되지만, 덕 타이핑은 객체가 필요한 동작을 충족시키면 호환성이 있다고 간주된다.
덕 타이핑의 예시로는 다음과 같은 코드를 들 수 있다.
class Duck {
quack() {
console.log("꽥꽥!");
}
}
class Robot {
quack() {
console.log("로봇이 꽥꽥 소리를 내냅니다.");
}
}
function makeSound(duckOrRobot: { quack: () => void }) {
duckOrRobot.quack();
}
const duck = new Duck();
const robot = new Robot();
makeSound(duck); // "꽥꽥!"
makeSound(robot); // "로봇이 꽥꽥 소리를 내냅니다."
위의 예제에서 makeSound
함수는 quack
메서드를 가지고 있는 객체를 인자로 받는다.
Duck
클래스와 Robot
클래스는 모두 quack
메서드를 가지고 있으므로, 이 두 클래스의 인스턴스 모두 makeSound
함수에 전달될 수 있다.
이는 덕타이핑의 특징이다.
Duck
클래스와 Robot
클래스는 서로 상속 관계가 아니며, 인터페이스를 구현하지도 않는다.
하지만 makeSound
함수에서 요구하는 동작 ( 즉, quack
메서드를 가지고 있음 ) 을 충족시키기 때문에 호환성이 있는 것으로 간주된다.
덕 타이핑은 동적 타입 언어에서 자주 사용되는 개념이지만, 타입스크립트의 구조적 타이핑도 덕 타이핑과 유사한 개념을 가지고 있다.
타입스크립트에서도 인터페이스나 클래스 상속 없이도 객체의 구조와 동작에 따라 타입 호환성을 판단할 수 있다.
하지만 타입스크립트는 정적 타입 언어이므로 컴파일 시간에 타입 체크를 수행한다.
즉, 구조적 타입은 타입 에러를 일으키는 반면, 덕 타입은 런타임 에러를 일으킨다고 볼 수 있다.
따라서 타입스크립트에서는 구조적 타이핑과 덕 타이핑의 장점을 살리면서도 컴파일 시간에 타입 안정성을 확보할 수 있다.
할당하는 값에는 객체 타입의 필수 속성이 있어야 한다.
"필수 속성"은 꼭 필요한 속성이다.
뒤에서 나오지만 "선택적 속성"이라는 것도 있다.
객체 타입에 필요한 멤버가 객체에 없다면 타입스크립트는 타입 오류를 발생시킨다.
type FirstAndLastNames = {
first: string;
last: string;
};
// ok
const hasBoth: FirstAndLastNames = {
first: "Sarojini",
last: "naidu",
};
// Error: Property 'last' is missing in type '{ first: string; }'
// but required in type 'FirstAndLastNames'.
const hasOnlyOne: FirstAndLastNames = {
first: "Sappho",
};
둘 사이에 일치하지 않는 타입도 허용하지 않는다.
객체 타입은 필수 속성 이름과 해당 속성이 예상되는 타입을 모두 지정한다.
객체의 속성의 타입이 일치하지 않으면 타입스크립트는 타입 오류를 발생시킨다.
type TimeRange = { start: Date };
const hasStartString: TimeRange = {
start: "2023-06-28",
// Error: Type 'string' is not assignable to type 'Date'.
};
변수가 객체 타입으로 선언되고, 초기값에 객체 타입에서 정의된 것보다 많은 필드가 있다면 타입스크립트에서 타입 오류가 발생한다.
type Poet = {
born: number;
name: string;
};
// ok
const poetMatch: Poet = {
born: 1928,
name: "Maya Angelou",
};
// Error: Type '{ activity: string; born: number; name: string; }' is not assignable to type 'Poet'.
// Object literal may only specify known properties, and 'activity' does not exist in type 'Poet'.
const extraProperty: Poet = {
activity: "walking",
born: 1935,
name: "Mary Oliver",
};
초과 속성 검사는 객체 타입으로 선언된 위치에서 생성되는 객체 리터럴에 대해서만 일어난다.
기존 객체 리터럴을 제공하면 초과 속성 검사를 우회한다.
type Poet = {
born: number;
name: string;
};
const extraProperty = {
activity: "walking",
born: 1935,
name: "Mary Oliver",
};
const extraPropertyButOk: Poet = extraProperty; // ok
다만, 주의해야 할 점이 있다.
위의 경우는 어디까지나 "우회" 이다.
초과 속성 검사를 하지않고 지나치는 것이기 때문에 자바스크립트 상에서의 extraPropertyButOk
객체는 다음과 같다.
{
activity: "walking",
born: 1935,
name: "Mary Oliver",
};
왜냐하면 위의 타입스크립트 코드는 결국 아래의 자바스크립트 코드와 같기 때문이다.
const extraPropertyButOk = extraProperty; // ok
하지만 extraPropertyButOk
객체가 자바스크립트 상에서 activity
속성이 있다고 하더라도 타입스크립트 상에서는 사용하면 타입 에러가 발생한다.
extraPropertyButOk
객체의 타입은 Poet
이고, 이 타입엔 activity
멤버가 없기 때문이다.
type Poet = {
born: number;
name: string;
};
const extraProperty = {
activity: "walking",
born: 1935,
name: "Mary Oliver",
};
const extraPropertyButOk: Poet = extraProperty; // ok
// Error: Property 'activity' does not exist on type 'Poet'.
console.log(extraPropertyButOk.activity);
자바스크립트 객체는 다른 객체의 멤버로 중첩될 수 있으므로 타입스크립트의 객체 타입도 타입 시스템에서 중첩된 객체 타입을 나타낼 수 있어야 한다.
type Poem = {
author: {
firstName: string;
lastName: string;
};
name: string;
};
const poemMatch: Poem = {
author: {
firstName: "Sylvia",
lastName: "Plath",
},
name: "Lady Lazarus",
};
const poemMismatch: Poem = {
// Error: Type '{ name: string; }' is not assignable
// to type '{ firstName: string; lastName: string; }'.
// Object literal may only specify known properties, and 'name'
// does not exist in type '{ firstName: string; lastName: string; }'.
author: {
name: "Sylvia Plath",
},
name: "Tulips",
};
Poem
타입을 작성할 때 author
속성의 형태를 자체 별칭 객체 타입으로 추출하는 방법도 있다.
type Author = {
firstName: string;
lastName: string;
};
type Poem = {
author: Author;
name: string;
};
중첩된 객체 타입을 고유한 타입 이름으로 바꿔서 사용하면 코드와 오류 메시지가 더 읽기 쉬워진다.
모든 객체에 객체 타입 속성이 필요한 건 아니다.
타입의 속성 애너테이션에서 :
앞에 ?
를 추가하면 선택적 속성임을 나타낼 수 있다.
type Book = {
author?: string;
pages: number;
};
// ok
const ok: Book = {
author: "Rita Dove",
pages: 80,
};
// ok
const ok2: Book = {
pages: 80,
};
ok2.pages; // 80
ok2.author; // undefined
// ok
const ok3: Book = {
author: undefined,
pages: 80,
};
ok3.pages; // 80
ok3.author; // undefined
// Error: Property 'pages' is missing in type '{ author: string; }' but required in type 'Book'.
const missing: Book = {
author: "Rita Dove",
};
선택적 속성과 undefined
를 포함한 유니언 타입의 속성 사이에는 차이가 있다는걸 유의하자.
?
를 사용해 선택적으로 선언된 속성은 존재하지 않아도 된다.
그럴 경우 해당속성은 undefined
가 자동으로 할당된다.
할당이야 자바스크립트 단에서 자동으로 할당되지만 string
타입을 가진 속성에 undefined
가 할당되었으니 나중에 존재하지 않은 속성에 접근할때 타입스크립트가 타입 에러를 발생시키지 않을까?
발생시키지않는다.
그 이유를 보자.
개발자가 모르는 사이에 타입뒤에 | undefined
가 붙어져있다.
따라서 타입에러를 발생시키지 않는다.
반면에 필수 속성과 | undefined
는 그 값이 undefined
라도 반드시 존재해야 한다.
type Writers = {
author: undefined;
};
const rquired: Writers = {author: undefined};
// Error: Property 'author' is missing in type '{}' but required in type 'Writers'.
const missingRequired: Writers = {};
변수에 여러 객체 타입 중 하나가 될 수 있는 초기값이 주어지면 타입스크립트는 해당 타입을 객체 타입 유니언으로 유추한다.
각각의 객체 타입에만 정의된 속성은 비록 초기값이 없는 선택적 타입이지만 각 객체 타입의 구성 요소로 주어진다.
const poem =
Math.random() > 0.5
? { name: "The Double Image", pages: 7 }
: { name: "Her Kind", rhymes: true };
// 타입
// {
// name: string;
// pages: number;
// rhymes?: undefined; // 초기값이 없는 선택적 속성
// } | {
// name: string;
// pages?: undefined; // 초기값이 없는 선택적 속성
// rhymes: boolean;
// }
객체 타입의 조합을 명시하면 객체 타입을 더 명확히 정의할 수 있다.
특히 값의 타입이 객체 타입으로 구성된 유니언이라면 타입스크립트의 타입 시스템은 이런 모든 유니언 타입에 존재하는 속성에 대한 접근만 허용한다.
type PoemWithPages = {
name: string;
pages: number;
};
type PoemWithRhymes = {
name: string;
rhymes: boolean;
};
type Poem = PoemWithPages | PoemWithRhymes;
const poem: Poem =
Math.random() > 0.5
? { name: "The Double Image", pages: 7 }
: { name: "Her Kind", rhymes: true };
poem.name; // ok
// Error: Property 'pages' does not exist on type 'Poem'.
// Property 'pages' does not exist on type 'PoemWithRhymes'.
poem.pages;
반면에 유추된 객체 타입에서는 선택적 속성으로 모든 속성이 주어진다고 배웠다.
따라서 유추된 객체 타입에서는 모든 속성에 대해 접근이 허용된다.const poem = Math.random() > 0.5 ? { name: "The Double Image", pages: 7 } : { name: "Her Kind", rhymes: true }; // ok poem.pages // undefined | number
타입 검사기가 유니언 타입 값에 특정 속성이 포함된 경우에만 코드 영역을 실행할 수 있음을 알게 되면, 값의 타입을 해당 속성을 포함하는 구성 요소로만 좁힌다.
if ("pages" in poem) {
poem.pages; // ok: poem은 PoemWithPages로 좁혀짐
} else {
poem.rhymes;// ok: poem은 PoemWithRhymes로 좁혀짐
}
타입스크립트는
if(poem.pages)
와 같은 형식으로 참 여부를 확인하는 것을 허용하지 않는다.
타입 오류를 발생시킨다.
객체의 속성에 객체의 형태를 포함하도록 하는 판별된 유니언 ( discriminated union )이라는 객체의 또 다른 인기있는 타입 형태가 있다.
타입스크립트는 코드에서 판별 속성을 사용해 타입 내로잉을 수행한다.
type PoemWithPages = {
name: string;
pages: number;
type: "pages"; // 판별값 : "pages"
};
type PoemWithRhymes = {
name: string;
rhymes: boolean;
type: "rhymes"; // 판별값 : "rhymes"
};
type Poem = PoemWithPages | PoemWithRhymes;
const poem: Poem =
Math.random() > 0.5
? { name: "The Double Image", pages: 7, type: "pages" }
: { name: "Her Kind", rhymes: true, type: "rhymes" };
if (poem.type === "pages") {
poem.pages;
} else {
poem.rhymes;
}
타입스크립트의 유니언 타입은 둘 이상의 다른 타입 중 하나의 타입이 될 수 있음을 나타낸다.
반면에 여러 타입을 동시에 나타내는 교차 타입도 있다.
교차 타입은 &
를 사용해 나타낸다.
교차 타입은 일반적으로 여러 기존 객체 타입을 별칭 객체 타입으로 결합해 새로운 타입을 생성한다.
type ArtwWork = {
genre: string;
name: string;
};
type Writing = {
pages: number;
name: string;
};
type WrittenArt = ArtwWork & Writing;
// 다음과 같다.
// {
// genre: string;
// name: string;
// pages: number;
// }
교차 타입은 유니언 타입과 결합할 수 있다.
type ShortPoem = { author: string } & (
| { kigo: string; type: "haiku" }
| { meter: number; type: "villanelle" }
);
// ok
const morningGlory: ShortPoem = {
author: "Fucuda Chiyo-ni",
kigo: "Morning Glory",
type: "haiku",
};
// Error: Type '{ author: string; type: "villanelle"; }'
// is not assignable to type 'ShortPoem'.
// Type '{ author: string; type: "villanelle"; }' is not assignable to type
// '{ author: string; } & { meter: number; type: "villanelle"; }'.
// Property 'meter' is missing in type '{ author: string; type: "villanelle"; }'
// but required in type '{ meter: number; type: "villanelle"; }'.
const oneArt: ShortPoem = {
author: "Elizabeth Bishop",
type: "villanelle",
};
교차 타입은 유용한 개념이지만, 개발자 스스로나 타입스크립트 컴파일러를 혼동시키는 방식으로 사용하기 쉽다.
교차 타입을 사용할 때는 가능한 한 코드를 간결하게 유지해야 한다.
유니언 타입과 결합하는 것처럼 복잡한 교차 타입을 만들게 되면 할당 가능성 오류 메시지는 읽기 어려워진다.
복잡한 교차 타입을 만들고자 한다면 일련의 별칭으로 된 객체 타입으로 분할하면 읽기가 훨씬 쉬워진다.
교차 타입은 잘못 사용하기 쉽고 불가능한 타입을 생성한다.
예를들어 원시 타입의 값은 동시에 여러 타입이 될 수 없기 때문에 교차 타입의 구성 요소로 함께 결합할 수 없다.
두 개의 원시 타입을 함께 시도하면 never
키워드로 표시되는 never
타입이 된다.
never
키워드와 never
타입은 프로그래밍 언어에서 bottom
타입 또는 empty
타입을 뜻한다.
bottom
타입은 값을 가질 수 없고 참조할 수 없는 타입이므로 bottom
타입에 그 어떠한 타입도 제공할 수 없다.
type NotPossible = number & string;
let notNUmber: NotPossible = 0;
// Error: Type 'number' is not assignable to type 'never'.
let notString: NotPossible = "";
// Error: Type 'string' is not assignable to type 'never'.
대부분의 타입스크립트 프로젝트는 never
타입을 거의 사용하지 않지만 코드에서 불가능한 상태를 나타내기 위해 가끔 등장한다.
하지만 대부분의 경우엔 교차 타입을 잘못 사용해 발생한 실수일 가능성이 높다.
[참고] : 러닝 타입스크립트 (한빛 미디어)