Open12
サバイバルTypeScript
TypeScriptの特徴
- 構造的型付けを採用している
- 名前的型付けではない(Java,Swift,C#,PHPなどは名前的型付け)
つまり下記のように記述してもコンパイルエラーが起こらない。
- 名前的型付けではない(Java,Swift,C#,PHPなどは名前的型付け)
class Person(){
walk(){}
}
class Dog(){
walk(){}
}
const person = new Person();
const dog: Dog = person;
これはDogとPersonクラスが構造として互換性があるため可能
// 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);
});
PostApiとPostApiに依存するPostServiceがありPostServiceのpostExistsをテストするケース。
構造的型付けのメリット
- getPost関数の実際のfetch処理を行わずにモックでテストを行うことができる。
→fetchで外部からデータを取得するようなテストはコケやすくモックで対応するのが一般的のよう。
constructor( api: PostApi){
this.api = api
}
この部分でDI(依存性注入)を行っている。
テストする側では
const service = new PostService(mockApi);
このようにモックしたPostApiのオブジェクトを引数として変数をインスタンスを作れば良い。
使う側で指定できる。
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);
});
依存の向きを変えることができる。
- 前: PostService→PostApi
- 後: PostService→IPostApi
PostServiceが詳細の実装内容に依存していた
具体が抽象に依存する形となる
PostApiに何かしら新しいメソッドやプロパティが追加された時にテストの方も変更する必要がある
インターフェースを活用するとPostApiに何か追加されてもその影響を受けない
(インターフェースが変更されたらもちろん影響受ける)
DIP、依存性逆転の原則
as const
const PRIVS = {
ADMIN = "admin",
NORMAL = "normal",
} as const;
こうするとPRIVSに意味わからん値を追加されるってことがなくなる
型ガード
function isDuck(animal: Animal): animal is Duck {
return animal instanceof Duck;
}
isDuck?
変数animalがDuckクラスのインスタンスであればanimalはDuck型だ!と教えている。
プリミティブ型
→イミュータブル(不変)
と
オブジェクト型が存在する
プリミティブ、原生的な、原始的な?それ以上分解できない型と理解したら良いだろうか
undefinedだけリテラルがない
nullは実はobject型