https://www.typescriptlang.org/docs/handbook/utility-types.html
기본으로 제공해주는 타입. 제네릭을 이용하여 주어진 타입을 변화시키는 방향으로 제공
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
type Test = Extract<string | number | (() => void), Function>; // type Test = () => void
function fn(arg: { a: number; b: string }): void;
type Test = Parameters<typeof fn>; // type Test = [arg: { a: number; b: string; }]
https://www.typescriptlang.org/docs/handbook/decorators.html
실험적 기능이라고 안내
tsconfig.json
의 compilerOptions.experimentalDecorators
를 설정해줘야 제대로 작동
{
"compilerOptions": {
...
"experimentalDecorators": true,
...
}
}
데코레이터는 @<expression>
의 형태로 작성하며 기본적으로 그 다음에 오는 클래스, 프로퍼티, 메서드, 파라미터를 전달받는다.
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
// 위와 같이 constructor에 대한 작업을 진행할 수도 있고 아래처럼 새로운 생성자를 정의할 수도 있다
return class extends constructor {
console.log('new!');
}
}
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
function decr (target, property, descriptor) {}
target
: 데코레이터를 적용할 대상property
: 그 대상의 프로퍼티descriptor
: 그 프로퍼티의 세부적인 정보, 프로퍼티 어트리뷰트에 대한 정보function decr (target, property, parameterIndex) {}
target
: 대상 객체, 생성자 함수 또는 클래스의 프로토타입..property
: 그 대상의 멤버 이름parameterIndex
: 그 매개변수의 indexinterface Document {
createElement(tagName: any): Element;
}
interface Document {
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
//////----------------------------------/////
interface Document {
// 아래처럼 리터럴 타입이 우선순위
createElement(tagName: "canvas"): HTMLCanvasElement;
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}
이때 namespace에 정의된 값들은 static 멤버로 여겨진다
하지만 namespace에 정의된 멤버를 클래스, 함수, enum에서 바라보고 싶다면 export 필수
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "!!!";
export let prefix = "Hello, ";
}
console.log(buildLabel("Sam")); // Hello, Sam!!!
이렇게 다른 위치에서 선언된 모듈을 import 하여 사용하면서 해당 모듈 자체에 대한 augmentation이 필요한 경우가 있을 수 있다. 다른 처리 없이 바로 하게되면 해당 모듈에서 그 부분을 정의한 적이 없기 때문에 정보를 알 수 없다. 작동이 안되는 것은 아니지만 타입 검사와 자동완성이 안되는 것.. 공식 문서의 예제가 잘 나와있어 첨부
// observable.ts
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) {
// ... another exercise for the reader
};
// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map((x) => x.toFixed());
그래서 위와 같이 map.ts
라는 새로운 파일에 declare로 해당 모듈에 보강할 내용에 대한 정보를 적어주고 사용할 곳에서 map.ts
를 같이 호출하게 되면 Observable 모듈의 class는 ./map속의 declare된 interface와 merge 되게 된다. 단 이때 export는 named export를 사용해야한다.
declare global {}
을 이용하면 전역 스코프에 대해 선언을 추가할 수 있다.
declare global {
interface Array<T> {
someMethod(): <T>;
}
}
Array.prototype.someMethod = function () {
// ...
};
다른 언어에서 쓰는 그 enum, 이름 붙여진 상수들의 집합
실제 런타임에 해당 member를 프로퍼티로 가지는 객체로 존재함
enum Direction { Up = 3421, Right, Down, Left} // 3421 3422 3423 3424
이름 Direction이 해당 enum의 타입을 의미
function test(param: Direction): void { ... };
test(Direction.Right);
enum에 할당할 수 있는 값들은 다음과 같다.
enum에는 지정된 값 말고도 계산되어야하는 값도 올 수 있다. 그러나 computed member는 constant member를 모두 선언해준 뒤에 해야한다.
constant member로 취급되는 것은 다음과 같다.
모든 member 가 constant일 때 union type처럼 생각할 수도 있다. 물론 정말 유니온 타입처럼 member 값만 넘겨줄 수는 없고 이를 이용하려면 keyof typeof
구문을 사용해야한다
enum E {
Foo,
Bar,
}
// E 가 constant member들로만 이루어진 enum이기 때문에 타입으로 사용 가능
function f1(x: E) { } // f1(E.Foo);
function f2(x: keyof typeof E) {} // f2('Foo')
constant member으로 이루어진 enum 키워드 앞에 const 를 붙여서 변화할 수 없는 enum을 만들 수도 있다. const를 사용하지 않는 일반 열거형과 다른 점은 컴파일 후에 enum의 흔적이 사라진다는 것. 해당 member들이 정의했던 constant 값으로 변경된다.
Iterable 값들을 위한 Iterable interface를 제공
// T타입으로 이루어진 순환가능한 객체(이터러블)를 배열로 변환해서 반환
// 이터러블에 스프레드 연산자를 사용 가능하기 때문에 아래와 같은 선언을 사용 가능#
function toArray<T>(xs: Iterable<T>): T[] {
return [...xs]
}
typescript는 jsx를 javascript로 변환하는 것도 지원한다. .tsx
확장자를 사용하고 config에서 jsx에 대한 옵션을 지정해주면 된다.
이를 사용하게 되면 원래 <type>
으로 설정하던 type assertion을 as type
으로 사용해야한다.
당연한 얘기지만 jsx → js 컴파일할 때의 규칙과 tsx → js의 규칙이 동일하다. 컴포넌트 생성시 사용하는 내장 함수가 다르기 때문에 사용자가 만든 element인 경우 그 이름이 대문자로 시작해야한다는 것
만약 소문자로 시작하는 내장 요소를 직접 지정하고 싶다면 JSX.IntrinsicElements
를 아래 declare를 이용하여 지정할 수 있다.
declare namespace JSX {
interface IntrinsicElements {
my: any,
el: {
// 어트리뷰트 값도 지정해줄 수 있음
// 옵셔널을 사용하지 않으면 필수 어트리뷰트가 됨
attr?: boolean,
}
}
}
<my />;
Value-based elements를 정의할 때는 함수형 컴포넌트 해석 → 클래스형 컴포넌트 해석 → 다 안되면 에러 의 과정을 거친다.
JSX.Element
에 할당 가능한 값(해당 타입을 포함해야함)을 반환하는 함수로 정의된다.JSX.ElementClass
에 할당 가능해야한다.위 예시에서 어트리뷰트 값을 지정해주는 것처럼 JSX.ElementChildrenAttribute
에 children 단일 프로퍼티를 명시하여 타입을 지정해줄 수 있지만 만약 React를 사용한다면 기본으로 제공해주는 children과 props 관련 타입들을 활용하는 것이 더 좋아보인다. 괜히 만들어두지 않았을 것…
제네릭을 활용하여 믹스인 함수를 정의하여 사용할 수도 잇음. 공식 문서에 나와있는 것은 클래스에 특정 프로퍼티를 추가해주는 예제
또한 클래스의 경우 인터페이스와 다르게 단일 상속만 가능하기 때문에 클래스를 정의하고 이후 mixin class를 이용하여 다중 상속 interface로 merging하면 다중상속된 클래스를 구현할 수 있다.
정적 속성을 사용하는 경우에는 아예 새로운 클래스를 반환하는 형태로 작성해야 믹스인을 이용하여 정의한 클래스들이 하나의 정적 속성을 공유하지 않는다(싱글톤 패턴이기 때문에 발생할 수 있는 문제)
esm을 node js에서도 사용할 수 있게 되면서 package.json을 정의할 때 module:
로 모듈 포맷을 명시해줬었는데 typescript를 사용하게되면 이 부분을 type:
으로 사용한다. 이에 따라 타입스크립트를 자바스크립트로 컴파일 할 때 모듈 포맷을 변경할 것인지 유지할 것인지 결정한다.
명시적인 것을 위해 .mts
확장자 지원
https://www.typescriptlang.org/docs/handbook/modules.html
esm처럼 import/export 구문을 사용가능하며 AMD나 Common JS의 workflow를 구현하기 위해 export =
문법도 지원한다. 이렇게 export하게되면 사용할 때 꼭 require()
문을 이용해야한다.
→ 작성된 것들이 컴파일될 때 지정된 모듈의 포맷으로 바뀜
타입스크립트로 작성되지 않은 모듈들도 분명히 존재.
그렇다면 사용하기 전에 해당 모듈들에 대해 타입을 정의해줘야함. .d.ts
이 이런 정의를 모아둔 파일. 일반적모든 모듈마다 하기보단 패키지당 하나씩 하는 경우가 많음 편리하니까..
declare module 'my-module' {
export interface {
name: string,
version: string,
}
export function customPrint(text: string): void;
}
만약 다른 파일로 정의해줬다면 해당 import 앞쪽에 /// <reference path="선언파일.d.ts"/>
을 적어줘서 해당 선언을 사용할 수 있다.
선언을 따로 작성하지 않고 그냥 export 하게된다면(shorthand ambient modules) any
타입을 가짐
모듈 import 시에 경로를 나타내는 방식에 따라 어떤 모듈인지 분석하는 방식 → node의 패키지/모듈 분석 방식을 따름
tsconfig.json에 baseUrl을 설정할 수 있으며 해당 위치에 존재하지 않을 수도 있는 모듈이지만 런타임에 맵핑되는 경로가 있다면 paths에 작성해줄 수 있다. 아래는 jquery의 예시다.
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // baseUrl에 상대적
}
또한 어떤 모듈을 exclude 하겠다고 tsconfig.json에 명시했더라도 컴파일되는 모듈에 import 되어있으면 exclude는 무시된다. → 컴파일 된다는 말
위에서 계속 말하긴 했지만 그래도 정리하면..
namespace 스코프를 만들어준다고 생각할 수 있다. 또한 공식문서의 note에서 말하듯이 파일 내부에 포함되어있는 내부 모듈을 namespace라고 부른다.
그렇기 때문에 위에서 모듈 설명했던 것처럼 여러 파일에 걸쳐 확장할 수 있다. 내부 모듈이니까
외부 모듈의 전체를 내부 모듈로 감싸는 것은 정말 불필요한 행위. 해당 외부 모듈에서 내부 모듈로 사용해야할 이유가 없는데 전체 코드를 네임스페이스로 감싸지 말자.
https://www.typescriptlang.org/docs/handbook/symbols.html es6에 추가된 심볼 사용법은 javascript와 동일
unique Symbol
을 이용하면 type annotation으로 symbol을 지정해줄 수 있으며 해당 식별자(변수, 함수 둘다 됨)은 const이거나 readonly static이어야 한다.
const sym: unique symbol = Symbol();
세 개의 슬래시 /// 를 사용하고 xml태그를 사용한 주석 → 컴파일할 때 지시어로 사용된다.
하지만 이는 파일의 상단에 있을 때만 적용되고 중간에 적혀있는 경우 그냥 주석으로 처리
/// <reference path="..." />
/// <reference types="..." />
/// <reference lib="..." />
/// <reference no-default-lib="true"/>
/// <amd-module name="..."/>
어떤 타입에 값을 할당할 때 해당 값이 그 타입과 완전히 일치하지 않아도 된다.
해당 타입이 포함된다면 할당 가능
type Person = { name: string };
const me: Person = { name: 'kim', age: 15 }; // 가능
함수의 경우에는 파라미터와 반환값을 고려해야하는데 포함관계가 약간 반대다
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
x의 파라미터가 y파라미터들에 포함되어 있으므로 y = x가능.
값이 전달되지 않을 수도 있는 것이 자바스크립트에선 자연스럽기 때문
let x = () => ({name: "Alice"});
let y = () => ({name: "Alice", location: "Seattle"});
y의 반환값이 x의 반환값을 포함하고 있다. 최소한 반환해야할 것은 반환하고 추가로 더 반환하는 것
x = y 가능
이런점 때문에 파라미터로 타입을 지정해줘도 해당 타입을 포함하는 타입이라면 파라미터로 전달이 가능하다
→ 사실 이런 현상을 막기위해 타입을 지정해주는 것이었는데 약간은 타입스크립트의 목적과 벗어나는 시나리오
→ 그치만 막지 않는 이유는 자바스크립트의 다양한 패턴이 이를 통해 이루어지기 때문
는 타입 비교를 할 때 정적 멤버에 대한 타입 비교는 실행하지 않고 인스턴스 멤버만 비교
class Animal {
feet: number;
constructor(name: string, numFeet: number) {}
}
class Size {
feet: number;
constructor(numFeet: number) {} // 생성자 역시 고려대상이 아님
}
let a: Animal;
let s: Size;
a = s; // 가능
s = a; // 가능
protected
와 private
멤버는 비교에 사용
의 경우는 타입인수를 적용한 구조가 같은지 아닌지를 따져서 호환성을 판단한다.
만약 타입인수가 정해지지 않은 제네릭 타입의 경우 해당 타입 인수를 any
로 여기고 호환성을 판단함
그러나 unknown 타입은 다른 것에 넣을 수 없음
any, unknown = 모든 타입
오직 unknown 또는 any = unknown
타입을 따로 지정해주지 않으면 기본적으로 알아서 타입을 추론한다.
배열에 대한 타입을 추론할 때는 최대한 해당 값들을 포괄할 수 있는 타입으로 추론을 하게 되는데
아래와 같은 경우 각 인스턴스들이 사실 Animal을 상속받았지만 주어진 정보만으로 얻은 타입들 중에는 다른 것들을 포함할 수 있는 타입이 없어 주석처럼 타입을 추론한다.
let zoo = [new Rhino(), new Elephant(), new Snake()];
// let zoo: (Rhino | Elephant | Snake)[]
// 원래는 let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()]; 이 가능
https://www.typescriptlang.org/docs/handbook/variable-declarations.html
자바스크립트와 동일