Open12

サバイバルTypeScript

TkmyztTkmyzt

TypeScriptの特徴

  • 構造的型付けを採用している
    • 名前的型付けではない(Java,Swift,C#,PHPなどは名前的型付け)
      つまり下記のように記述してもコンパイルエラーが起こらない。
class Person(){
    walk(){}
}

class Dog(){
    walk(){}
}

const person = new Person();
const dog: Dog = person;
TkmyztTkmyzt

これはDogとPersonクラスが構造として互換性があるため可能

TkmyztTkmyzt
// post.ts
export type Post {
    userId: number;
    id: number;
    title: string;
    body: string;
}

export class PostApi {
    async getPost(id: number): Promise<Post> {
        const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
        const post = ( await response.json() ) as Post;
        return post;
    }
}

export class PostService {
  private api: PostApi;
  constructor(api: PostApi){
      this.api = api;  
  }
  async postExists(id: number): Promise<boolean> {
      const post = await this.api.getPost(id);
      return post !== null && post !== undefined;  
  }
}
// post.test.ts
import { Post, PostApi, PostService } from "./index";
test("postExists", async () => {
  const mockApi: PostApi = {
    async getPost(id: number): Promise<Post> {
      return {
        userId: 1,
        id: 1,
        title: "test",
        body: "test",
      };
    },
  };
  const service = new PostService(mockApi);
  const result = await service.postExists(1);

  expect(result).toBe(true);
});
    
TkmyztTkmyzt

PostApiとPostApiに依存するPostServiceがありPostServiceのpostExistsをテストするケース。
構造的型付けのメリット

  • getPost関数の実際のfetch処理を行わずにモックでテストを行うことができる。
    →fetchで外部からデータを取得するようなテストはコケやすくモックで対応するのが一般的のよう。
TkmyztTkmyzt
constructor( api: PostApi){
    this.api = api
}

この部分でDI(依存性注入)を行っている。
テストする側では

const service = new PostService(mockApi);

このようにモックしたPostApiのオブジェクトを引数として変数をインスタンスを作れば良い。
使う側で指定できる。

TkmyztTkmyzt

Interfaceを利用してもっといい感じのコードにできる

export type Post {
    userId: number;
    id: number;
    title: string;
    body: string;
}

export interface IPostApi {
    getPost(id: number): Promise<Post>
}

export class PostApi implements IPostApi {
    async getPost(id: number): Promise<Post>{
        const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
        const post = ( await response.json() ) as Post;
        return post;
    }
}

export class PostService {
    private api: PostApi;
    constructor(api: IPostApi){
        this.api = api;
    }
    async postExists(id: number): Promise<boolean> {
      const post = await this.api.getPost(id);
      return post !== null && post !== undefined;  
  }
}

// index.test.ts

import { Post, IPostApi, PostService } from "./index"; // importはそのまま
test("postExists", async () => {
  const mockApi: IPostApi = {
    async getPost(id: number): Promise<Post> {
      return {
        userId: 1,
        id: 1,
        title: "test",
        body: "test",
      };
    },
  };
  const service = new PostService(mockApi);
  const result = await service.postExists(1);

  expect(result).toBe(true);
});

TkmyztTkmyzt

依存の向きを変えることができる。

  • 前: PostService→PostApi
  • 後: PostService→IPostApi
    PostServiceが詳細の実装内容に依存していた
    具体が抽象に依存する形となる
    PostApiに何かしら新しいメソッドやプロパティが追加された時にテストの方も変更する必要がある
    インターフェースを活用するとPostApiに何か追加されてもその影響を受けない
    (インターフェースが変更されたらもちろん影響受ける)
TkmyztTkmyzt
as const

const PRIVS = {
    ADMIN = "admin",
    NORMAL = "normal",
}  as const;

こうするとPRIVSに意味わからん値を追加されるってことがなくなる

TkmyztTkmyzt

型ガード

function isDuck(animal: Animal): animal is Duck {
    return animal instanceof Duck;
}

isDuck?
変数animalがDuckクラスのインスタンスであればanimalはDuck型だ!と教えている。

TkmyztTkmyzt

プリミティブ型
→イミュータブル(不変)

オブジェクト型が存在する

プリミティブ、原生的な、原始的な?それ以上分解できない型と理解したら良いだろうか
undefinedだけリテラルがない