Kotlin

TypeScript 시작하기

팅리엔 2021. 9. 8. 09:49

Why TypeScript?

function fn(x) {
    return x.flip();
}

자바스크립트는 런타임에 타입을 확인하고 무엇을 할지 결정한다.
위 코드에서 fn은 특정한 타입의 x를 받아야 하고, x가 반드시 callable한 flip를 가지고 있어야 한다.
이로 인해 시스템의 동작을 예측하기 어려워지고, 프로그래머의 실수를 런타임에야 발견하게 된다.

tsc

  • TypeScript compiler
  • 설치하기: npm install -g typescript
tsc hello.ts
  • 타입을 체크한다.
  • hello.js라는 새로운 파일이 생성된다.
  • 에러가 있더라도 hello.js 파일이 생성된다. (당신이 TypeScript보다 잘 알 것이기에)
  • 에러가 있을 때 컴파일을 하고 싶지 않다면 --noEmitOnError 옵션을 준다.
  • 타입스크립트 그대로 실행할 수 있는 브라우저/런타임은 없기에 먼저 컴파일을 해줘야 하는 것이다.
  • hello.js는 낡은 버전의 자바스크립트로 변환된다. (default는 ES3) --target 옵션을 주어 변환 버전을 지정할 수 있다.
  • 요즘엔 ES2015가 대세이기에 특별히 오래된 브라우저를 지원하고 싶지 않은 한 --target ES2015를 명시해준다.

Explit Types

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

greet("Maddison", new Date());

그러나 타입을 반드시 명시할 필요는 없다.

Strictness

타입스크립트가 타입을 얼마나 엄격하게 검사할 것인지 지정할 수 있다.

  • noImplicitAny: 몇몇 경우에서 타입스크립트는 타입을 추론하지 않고 any로 간주한다.
    이를 막기 위해 any로 간주되는 경우 오류를 발생시킨다.
  • strictNullChecks: nullundefinded를 보다 명시적으로 처리시킨다.

Basic Types

Primitive Types

  • string
  • number
  • boolean

Array

string[] (= Array)

Any

  • 특정 값으로 인해 타입 검사 오류를 발생시키고 싶지 않을 때 사용
  • any를 사용하면 추가적인 타입 검사가 비활성화 된다.

Object

function printName(obj: { first: string; last: string }) {  
    // …  
}  

Optional

function printName(obj: { first: string; last?: string }) {  
    // …  
}  

Union

function printName(name: string | number) {  
    // …  
}  
  • ‘무엇이든 하나’
  • 모든 멤버에 유효한 작업만 허용 (위 코드에서 string 타입에만 유효한 메서드는 호출할 수 없다)

Type Aliases

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

function printCoord(pt: Point) {
    console.log(pt.x);
    console.log(pt.y);
}

printCoord({ x: 100, y: 100 });

똑같은 타입을 재사용하거나 다른 이름으로 쓰고 싶은 경우

Interfaces

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

function printCoord(pt: Point) {
    console.log(pt.x);
    console.log(pt.y);
}

printCoord({ x: 100, y: 100 });

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

  • 인터페이스는 확장 가능하다.
  • 타입 별칭은 intersection을 통해 확장한다.
interface Animal {  
    name: string  
}  

interface Bear extends Animal {  
    honey: boolean  
}

const bear = getBear()  
bear.name  
bear.honey

interface Window {  
    title: string  
}

interface Window {  
    ts: TypeScriptAPI  
}

const src = ‘const a = "Hello world"’;  
window.ts.transpileModule(src, {});

type Animal = {  
    name: string  
}

type Bear = Animal & {  
    honey: boolean  
}

const bear = getBear()  
bear.name  
bear.honey

우선 인터페이스를 사용하고 이후 문제가 생기면 타입을 사용한다.

Type Assertions

const myCanvas = document.getElementById("main canvas") as HTMLCanvasElement;

# 또는

const myCanvas = document.getElementById("main canvas");
  • 당신이 타입스크립트보다 타입에 대해 더 잘 아는 경우
  • 타입을 좀 더 구체적으로 명시할 수 있다.
  • 물론 불가능한 강제 변환은 안 된다. (const x = "hello" as number;)

Literal

let x: "hello" = "hello";  
// ok  
x = "hello";  
// err!  
x = "hello2";

값의 범위를 제한할 때 사용하면 유용하다.

function printText(s: string, alignment: "left" | "right" | "center") { … }

리터럴 추론시 문제

const req = { url: "https://example.com", method: "GET" };  
handleRequest(req.url, req.method); // err!

function handleRequest(url: string, method: "GET" | "POST") { … }

req.method의 타입이 "GET"이 아닌 string으로 추론되어 에러가 발생한다.
req 생성 시점과 handleRequest 호출 시점 사이에 코드 평가가 발생할 수도 있고, 이때 req.method에 새로운 문자열이 대입될 수도 있기 때문이다.
이 문제는 아래와 같은 방법으로 해결한다.

const req = { url: "https://example.com", method: "GET" as "GET" };  
handleRequest(req.url, req.method as "GET");

# 또는

declare function handleRequest(url: string, method: "GET" | "POST"): void;

const req = { url: "https://example.com", method: "GET" } as const;  
handleRequest(req.url, req.method);

as const는 해당 객체의 모든 프로퍼티에 리터럴 타입의 값이 대입되도록 보장한다.

null, undefined

항상 strictNullChecks 옵션을 설정하여 null, undefined를 체크하도록 한다.

  • strictNullChecks가 설정되어 있으면 해당 값을 사용하기 전에 테스트를 해야한다. (if (x === undefined) { … })
  • null, undefined가 아니라고 단언하려면 표현식 뒤에 !를 붙인다.

Enums

Less common primitives

  • bitint
  • symbol: globally unique reference
const firstName = Symbol("name");
const secondName = Symbol("name");

if (firstName === secondName) {
    // the condition is always ‘false’
}

Type guard

function padLeft(padding: number | string, input: string) {
    if (typeof padding === "number") {
        return (padding + 1).join(" ") + input;
    }

    return padding + input;
}
  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

False

  • 0
  • NaN
  • ""
  • 0n (the bigint version of zero)
  • null
  • undefined

Functions

function greeter(fn: (name: string) => void) {
    fn(`Hello, ${name}`);
}

greeter(console.log("Ariana"));

Call Signatures

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

function doSomething(fn: DescribableFunction) {
    console.log(fn.description + " returned" + fn(6));
}

와 ㅇ0ㅇ

Construct Signatures

type SomeConstructor = {
    new (s: string): SomeObject;
};

function fn(ctor: SomeConstructor) {
    return new ctor("hello");
}

call, construnct signates를 합치면

interface CallOrConstruct {
    new (s: string): Date;
    (n?: number): number;
}

Generic Functions

function firstElement(arr: any[]) {
    return arr[0];
}

위 function은 any 타입을 리턴하기에, 이를 개선하면

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

타입 값에 제한을 두고 싶으면

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

longest([1, 2], [1, 2, 3]);
longest("a", "aaa");
longest(10, 100); //err!
  • 가능한 타입 값에 제한을 두지 않고 쓰도록 한다.
  • 혹시 불필요한 제네릭이 아닌지 확인한다.

Overloads

function makeDate(timestamp: number): Date; 
function makeDate(m: number, d: number, y:number): Date; 
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
    if (d !== undefined && y !== undefined) { 
        return new Date(y, mOrTimestamp, d); 
    } else { 
        return new Date(mOrTimestamp); 
    } 
}

makeDate(12345678);  
makeDate(5, 5, 5);  
makeDate(1, 3); //err!
  • 바디를 구현한 function을 직접 호출할 수는 없다.
    The signature of the implementation is not visible from the outside.
  • 가능한 오버로딩보다 union type을 사용하도록 한다. (타입에 유연성을 주기 위해)

Declaring this in a Function

interface DB {
    filterUsers(filter: (this: User) => boolean): User[];
}

const db = getDB();
const admins = db.filterUsers(function (this: User)) {
    return this.admin;
}
  • function에서 this가 무엇을 가르키는지 명시적으로 표시할 수 있다.
  • arrow function에서는 이런 방식을 사용할 수 없다.

Other types working with function types

  • void
  • object
  • unknown: represents any value but is safer because it can't do anything
  • never: represents that the function throws an exception or terminates execution of the program
  • Function: describes properties like bind, call, apply

Object

readonly properties

interface SomeType {
    readonly prop: string;
}
interface Person {
    name: string;
    age: number;
}

interface ReadonlyPerson {
    readonly name: string;
    readonly age: number;
}

let writablePerson: Person = {
    name: "Ariana",
    age: 16,
};

let readonlyPerson: ReadonlyPerson = writablePerson;

console.log(readonlyPerson.age); //16
writeablePerson.age++;
console.log(readonlyPerson.age); //17

Index Signatures

interface StringArray {
    [index: number]: string;
}

const myArray: StringArray = getStringArray();
const secondItem = myArray[1]; //secondItem's type is string
interface NumberDictionary {
    [index: string]: number; //인덱스는 string, 그 값은 number 타입이어야 한다.

    length: number;
    name: string; //err! 값이 number 타입이어야 한다.
}

Extending Types

interface Address {
    name: string;
    street: string;
    city: string;
}

interface AddressWithUnit extends Address {
    unit: string;
}
  • 인터페이스를 여러개 상속할 수 있다.

Intersection Types

interface Colorful {
    color: string;
}

interface Circle {
    radius: number;
}

type ColorfulCircle = Colorful & Circle;

function draw(circle: Colorful & Circle) {
    console.log(`${circle.color} ${circle.radius}`);
}

Generic Object Types

type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

Pair

function doSomething(pair: [string, number]) {
    const a = pair[0]; //string
    const b = pair[1]; //number
}

Type Manipulation

keyof

object의 key들의 lieteral 값들을 가져온다.
뭔지 잘 감이 안 온다.
key를 사용하고 싶을때 key만을 enum처럼 뽑아 type으로 사용하는 것이라 이해했다. ???맞나?

Indexed Access Types

type Person = { age: number; name: string; alive: boolean };
type t1 = Person["age"]; //number
type t2 = Person["age" | "name"]; //string | number

Template Literal Types

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; //"welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

Class

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

const pt = new Point();
pt.x = 0;
pt.y = 0;
class Point {
    x = 0; //초기화
    y = 0;
}

const pt = new Point();
console.log(`${pt.x}, pt.y`); //0, 0

--strictPropertyInitialization 옵션을 지정하면 클래스 필드들은 반드시 생성자에서 초기화되어야 한다.

class GoodGreeter {
    name: string;

    constructor() {
        this.name = "hello";
    }
}

생성자가 아닌 다른 수단을 통해 필드를 확실히 초기화하려는 경우(예를 들어, 외부 라이브러리가 클래스의 일부를 채우고 있을 수 있음) ! 연산자를 사용한다.

class OKGreeter {
  // Not initialized, but no error
  name!: string;
}

Constructors

class Point {
  // Overloads
  constructor(x: number, y: string);
  constructor(s: string);
  constructor(xs: any, y?: any) {
    // TBD
  }
}

생성자에서 super()는 this에 접근하기 전에 호출되어야 한다.

Class Heritage

  • implements interface
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10; //err! Property 'y' does not exist on type 'C'.
interface Pingable { 
	ping(): void; 
} 

class Sonar implements Pingable { 
    ping() {
        console.log("ping!");
    }
}
  • extends class
class Animal { 
    move() { 
        console.log("Moving along!"); 
    } 
} 

class Dog extends Animal { 
    woof(times: number) { 
        for (let i = 0; i < times; i++) { 
            console.log("woof!"); 
        } 
    } 
} 

const d = new Dog(); // Base class method 
d.move(); // Derived class method d.woof(3);

 

 

Member Visibility

  • public: default
  • protected: subclasses에서만 접근 가능
  • private

+주의! protected, private은 타입 체킹 시에만 강제된다. 런타임 시에는 접근 가능하다.

class MySafe {
    private secretKey = 12345;
}

const s = mySafe();

console.log(s["secretKey"]; //ok
console.log(s.secretKey); //err! Property 'secretKey' is private and only accessible within class 'MySafe'.

Static Members

class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}
console.log(MyClass.x);
MyClass.printX();

this at Runtime in Classes

참고: https://poiemaweb.com/es6-arrow-function

class MyClass {
  name = "MyClass";
  getName() {
    return this.name;
  }
}
const c = new MyClass();
const obj = {
  name: "obj",
  getName: c.getName,
};

// Prints "obj", not "MyClass"
console.log(obj.getName());

자바스크립트에서 함수 안에서의 this의 값은 함수가 어떻게 호출되느냐에 따라 다르다. this에 바인딩될 객체가 동적으로 결정된다.

위 코드에서 함수가 obj 레퍼런스를 통해 호출되었기 때문에 this는 c가 아니라 obj이다.

class MyClass {
  name = "MyClass";
  getName = () => {
    return this.name;
  }
}
const c = new MyClass();
const obj = {
  name: "obj",
  getName: c.getName,
};

// Prints "MyClass", not "obj"
console.log(obj.getName());

화살표 함수를 사용하면 this에 바인딩될 객체가 정적으로 결정된다. 화살표 함수의 this는 언제나 상위 스코프의 this를 가리킨다. (Lexical this)