Type Scriptテクニック
22568 ワード
前言
以前からTypeScriptを用いて日常的な符号化を試みてきたが,静的タイプ言語に対する自分の理解が深くなく,TypeScriptのタイプシステムに一定の複雑さがあるため,この言語の優位性を十分に発揮していないと感じ,コードの可読性とメンテナンス性を向上させた.そこでここ数日、この言語をよく研究し、特別な言語特性と実用的なテクニックをまとめたいと思っています.
オペレータ
typeof-変数のタイプを取得
keyof-取得タイプのキー
組合せtypeofとkeyof-キャプチャキーの名前
in-遍歴キー名
特殊なタイプ
ネストされたインタフェースのタイプ
条件タイプ
辞書の種類
infer-遅延推定タイプ
よく使うテクニック
const enumを使用して定数リストを維持する
Partial & Pick
Exclude & Omit
巧みなneverタイプ
ハイブリッドクラス(mixins)
タイプ変換
次はLeetCodeのType Scriptに関する面接問題を解きます.テーマの大まかな内容はEffectModuleというクラスで、その実現は以下の通りです.
次に、connect関数がEffectModuleクラスのインスタンスオブジェクトをパラメータとして受信し、新しいオブジェクトが返されます.
接続関数を呼び出すと、返される新しいオブジェクトにはEffectModuleと同じ名前のメソッドしか含まれておらず、メソッドのタイプ署名が変更されていることがわかります.クラスから関数 を選択巧みにinferでタイプ変換 次は私がこの問題を解く答えです.
制御反転と依存注入
制御反転(Inversion of Control)と依存注入(Dependency Injection)はオブジェクト向けプログラミングにおいて非常に重要な思想と法則である.Wikipediaでは、IoCはコンピュータコード間の結合度を低下させることができ、DIは、1つのオブジェクトが作成されたときに、そのオブジェクトに依存するすべてのオブジェクトを注入するプロセスを表す.フロントエンドフレームAngularとNode.jsのバックエンドフレームワークNestはこの考えを引用した.この2つの概念の具体的な説明はここでは展開されないが、読者はこの2つの文章を見ることができる[1][2].次に,Angular 5以前のDependency Injectionに基づいて,簡易版の制御反転と依存注入を実現する.
まず、関連するテストコードを作成します.
実装するコア機能は3つあります.提供されたクラスに基づいてIoCコンテナを作成する、クラス間の依存関係を管理することができる .は、IoCコンテナを介してクラスのインスタンスオブジェクトを取得する際に、関連する依存オブジェクト を注入する.マルチレベル依存と処理エッジ状況 を実現する.
まず、最も簡単な
次に、提供されたproviderクラスに基づいて、クラス間の依存関係を管理できるIoCコンテナを作成します.
上記のコードから、IoCコンテナが作成されると提供される各クラスおよびそのクラスに依存する他のクラスが
次に、IoCコンテナからクラスのインスタンスオブジェクトを取得するロジックを実装します.
ここで、上記のコードを振り返ると、マルチレベル依存の機能はすでに実現されています.loCコンテナを初期化すると、クラスの依存関係しか見つかりません.依存クラスによってより深い依存関係を見つけることはできませんが、提供されたクラスごとに同じ操作を実行しているので、自然に複数のクラス間の依存関係が実現されます.
エッジの場合も対応する処理を行いました.例えば、提供されたproviderクラスは空の配列であり、クラスは
これにより,制御反転と依存注入のコア機能との差は少なく,残りはインタフェース定義コード,さらに
以前からTypeScriptを用いて日常的な符号化を試みてきたが,静的タイプ言語に対する自分の理解が深くなく,TypeScriptのタイプシステムに一定の複雑さがあるため,この言語の優位性を十分に発揮していないと感じ,コードの可読性とメンテナンス性を向上させた.そこでここ数日、この言語をよく研究し、特別な言語特性と実用的なテクニックをまとめたいと思っています.
オペレータ
typeof-変数のタイプを取得
const colors = {
red: 'red',
blue: 'blue'
}
// type res = { red: string; blue: string }
type res = typeof colors
keyof-取得タイプのキー
const data = {
a: 3,
hello: 'world'
}
//
function get<T extends object, K extends keyof T>(o: T, name: K): T[K] {
return o[name]
}
get(data, 'a') // 3
get(data, 'b') // Error
組合せtypeofとkeyof-キャプチャキーの名前
const colors = {
red: 'red',
blue: 'blue'
}
type Colors = keyof typeof colors
let color: Colors // 'red' | 'blue'
color = 'red' // ok
color = 'blue' // ok
color = 'anythingElse' // Error
in-遍歴キー名
interface Square {
kind: 'square'
size: number
}
// type res = (radius: number) => { kind: 'square'; size: number }
type res = (radius: number) => { [T in keyof Square]: Square[T] }
特殊なタイプ
ネストされたインタフェースのタイプ
interface Producer {
name: string
cost: number
production: number
}
interface Province {
name: string
demand: number
price: number
producers: Producer[]
}
let data: Province = {
name: 'Asia',
demand: 30,
price: 20,
producers: [
{ name: 'Byzantium', cost: 10, production: 9 },
{ name: 'Attalia', cost: 12, production: 10 },
{ name: 'Sinope', cost: 10, production: 6 }
]
}
interface Play {
name: string
type: string
}
interface Plays {
[key: string]: Play
}
let plays: Plays = {
'hamlet': { name: 'Hamlet', type: 'tragedy' },
'as-like': { name: 'As You Like It', type: 'comedy' },
'othello': { name: 'Othello', type: 'tragedy' }
}
条件タイプ
type isBool = T extends boolean ? true : false
// type t1 = false
type t1 = isBool
// type t2 = true
type t2 = isBool<false>
辞書の種類
interface Dictionary {
[index: string]: T
}
const data: Dictionary = {
a: 3,
b: 4,
}
infer-遅延推定タイプ
type ParamType = T extends (param: infer P) => any ? P : T
interface User {
name: string
age: number
}
type Func = (user: User) => void
type Param = ParamType // Param = User
type AA = ParamType // string
type ElementOf = T extends Array ? E : never
type TTuple = [string, number]
type ToUnion = ElementOf // string | number
よく使うテクニック
const enumを使用して定数リストを維持する
const enum STATUS {
TODO = 'TODO',
DONE = 'DONE',
DOING = 'DOING'
}
function todos(status: STATUS): Todo[] {
// ...
}
todos(STATUS.TODO)
Partial & Pick
type Partial = {
[P in keyof T]?: T[P]
}
type Pick = {
[P in K]: T[P]
}
interface User {
id: number
age: number
name: string
}
// type PartialUser = { id?: number; age?: number; name?: string }
type PartialUser = Partial
// type PickUser = { id: number; age: number }
type PickUser = Pick'id'|'age'>
Exclude & Omit
type Exclude = T extends U ? never : T
// type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>
type Omit = Pick>
interface User {
id: number
age: number
name: string
}
// type PickUser = { age: number; name: string }
type OmitUser = Omit'id'>
巧みなneverタイプ
type FunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]
interface Part {
id: number
name: string
subparts: Part[]
updatePart(newName: string): void
}
type T40 = FunctionPropertyNames // 'updatePart'
type T41 = NonFunctionPropertyNames // 'id' | 'name' | 'subparts'
ハイブリッドクラス(mixins)
// mixins
type Constructor = new (...args: any[]) => T
//
function TimesTamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now()
}
}
//
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActivated = false
activate() {
this.isActivated = true
}
deactivate() {
this.isActivated = false
}
}
}
//
class User {
name = ''
}
// TimesTamped User
const TimestampedUser = TimesTamped(User)
// TimesTamped Activatable
const TimestampedActivatableUser = TimesTamped(Activatable(User))
//
const timestampedUserExample = new TimestampedUser()
console.log(timestampedUserExample.timestamp)
const timestampedActivatableUserExample = new TimestampedActivatableUser()
console.log(timestampedActivatableUserExample.timestamp)
console.log(timestampedActivatableUserExample.isActivated)
タイプ変換
次はLeetCodeのType Scriptに関する面接問題を解きます.テーマの大まかな内容はEffectModuleというクラスで、その実現は以下の通りです.
interface Action {
payload?: T
type: string
}
class EffectModule {
count = 1
message = 'hello!'
delay(input: Promise) {
return input.then(i => ({
payload: `hello ${i}!`,
type: 'delay'
}))
}
setMessage(action: Action<Date>) {
return {
payload: action.payload.getMilliseconds(),
type: 'set-message'
}
}
}
次に、connect関数がEffectModuleクラスのインスタンスオブジェクトをパラメータとして受信し、新しいオブジェクトが返されます.
const connect: Connect = _m => ({
delay: (input: number) => ({
type: 'delay',
payload: `hello ${input}`
}),
setMessage: (input: Date) => ({
type: 'set-message',
payload: input.getMilliseconds()
})
})
type Connected = {
delay(input: number): Action
setMessage(action: Date): Action
}
const connected: Connected = connect(new EffectModule())
接続関数を呼び出すと、返される新しいオブジェクトにはEffectModuleと同じ名前のメソッドしか含まれておらず、メソッドのタイプ署名が変更されていることがわかります.
asyncMethod(input: Promise): Promise>
asyncMethod(input: T): Action
syncMethod(action: Action): Action
syncMethod(action: T): Action
type Connect = (module: EffectModule) => any
を作成して、最終的なコンパイルをスムーズに通過させる必要があります.このテーマは主に2つの点を考察していることがわかります.type FuncName = { [P in keyof T]: T[P] extends Function ? P : never }[keyof T]
type Middle = { [T in FuncName]: EffectModule[T] }
type Transfer = {
[P in keyof T]: T[P] extends (input: Promise) => Promise
? (input: J) => K
: T[P] extends (action: Action) => infer K
? (input: J) => K
: never
}
type Connect = (module: EffectModule) => { [T in keyof Transfer]: Transfer[T] }
制御反転と依存注入
制御反転(Inversion of Control)と依存注入(Dependency Injection)はオブジェクト向けプログラミングにおいて非常に重要な思想と法則である.Wikipediaでは、IoCはコンピュータコード間の結合度を低下させることができ、DIは、1つのオブジェクトが作成されたときに、そのオブジェクトに依存するすべてのオブジェクトを注入するプロセスを表す.フロントエンドフレームAngularとNode.jsのバックエンドフレームワークNestはこの考えを引用した.この2つの概念の具体的な説明はここでは展開されないが、読者はこの2つの文章を見ることができる[1][2].次に,Angular 5以前のDependency Injectionに基づいて,簡易版の制御反転と依存注入を実現する.
まず、関連するテストコードを作成します.
import { expect } from 'chai'
import { Injectable, createInjector } from './injection'
class Engine {}
class DashboardSoftware {}
@Injectable()
class Dashboard {
constructor(public software: DashboardSoftware) {}
}
@Injectable()
class Car {
constructor(public engine: Engine) {}
}
@Injectable()
class CarWithDashboard {
constructor(public engine: Engine, public dashboard: Dashboard) {}
}
class NoAnnotations {
constructor(_secretDependency: any) {}
}
describe('injector', () => {
it('should instantiate a class without dependencies', () => {
const injector = createInjector([Engine])
const engine = injector.get(Engine)
expect(engine instanceof Engine).to.be.true
})
it('should resolve dependencies based on type information', () => {
const injector = createInjector([Engine, Car])
const car = injector.get(Car)
expect(car instanceof Car).to.be.true
expect(car.engine instanceof Engine).to.be.true
})
it('should resolve nested dependencies based on type information', () => {
const injector = createInjector([CarWithDashboard, Engine, Dashboard, DashboardSoftware])
const _CarWithDashboard = injector.get(CarWithDashboard)
expect(_CarWithDashboard.dashboard.software instanceof DashboardSoftware).to.be.true
})
it('should cache instances', () => {
const injector = createInjector([Engine])
const e1 = injector.get(Engine)
const e2 = injector.get(Engine)
expect(e1).to.equal(e2)
})
it('should show the full path when no provider', () => {
const injector = createInjector([CarWithDashboard, Engine, Dashboard])
expect(() => injector.get(CarWithDashboard)).to.throw('No provider for DashboardSoftware!')
})
it('should throw when no type', () => {
expect(() => createInjector([NoAnnotations])).to.throw(
'Make sure that NoAnnotations is decorated with Injectable.'
)
})
it('should throw when no provider defined', () => {
const injector = createInjector([])
expect(() => injector.get('NonExisting')).to.throw('No provider for NonExisting!')
})
})
実装するコア機能は3つあります.
まず、最も簡単な
@Injectable
装飾器を実現します.export const Injectable = (): ClassDecorator => target => {
Reflect.defineMetadata('Injectable', true, target)
}
次に、提供されたproviderクラスに基づいて、クラス間の依存関係を管理できるIoCコンテナを作成します.
abstract class ReflectiveInjector implements Injector {
abstract get(token: any): any
static resolve(providers: Provider[]): ResolvedReflectiveProvider[] {
return providers.map(resolveReflectiveProvider)
}
static fromResolvedProviders(providers: ResolvedReflectiveProvider[]): ReflectiveInjector {
return new ReflectiveInjector_(providers)
}
static resolveAndCreate(providers: Provider[]): ReflectiveInjector {
const resolvedReflectiveProviders = ReflectiveInjector.resolve(providers)
return ReflectiveInjector.fromResolvedProviders(resolvedReflectiveProviders)
}
}
class ReflectiveInjector_ implements ReflectiveInjector {
_providers: ResolvedReflectiveProvider[]
keyIds: number[]
objs: any[]
constructor(_providers: ResolvedReflectiveProvider[]) {
this._providers = _providers
const len = _providers.length
this.keyIds = new Array(len)
this.objs = new Array(len)
for (let i = 0; i this.keyIds[i] = _providers[i].key.id
this.objs[i] = undefined
}
}
// ...
}
function resolveReflectiveProvider(provider: Provider): ResolvedReflectiveProvider {
return new ResolvedReflectiveProvider_(
ReflectiveKey.get(provider),
resolveReflectiveFactory(provider)
)
}
function resolveReflectiveFactory(provider: Provider): ResolvedReflectiveFactory {
let factoryFn: Function
let resolvedDeps: ReflectiveDependency[]
factoryFn = factory(provider)
resolvedDeps = dependenciesFor(provider)
return new ResolvedReflectiveFactory(factoryFn, resolvedDeps)
}
function factory<T>(t: Type): (args: any[]) => T {
return (...args: any[]) => new t(...args)
}
function dependenciesFor(type: Type): ReflectiveDependency[] {
const params = parameters(type)
return params.map(extractToken)
}
function parameters(type: Type) {
if (noCtor(type)) return []
const isInjectable = Reflect.getMetadata('Injectable', type)
const res = Reflect.getMetadata('design:paramtypes', type)
if (!isInjectable) throw noAnnotationError(type)
return res ? res : []
}
export const createInjector = (providers: Provider[]): ReflectiveInjector_ => {
return ReflectiveInjector.resolveAndCreate(providers) as ReflectiveInjector_
}
上記のコードから、IoCコンテナが作成されると提供される各クラスおよびそのクラスに依存する他のクラスが
ResolvedReflectiveProvider_
のインスタンスオブジェクトとしてコンテナに格納され、外部に返されるのはコンテナオブジェクトReflectiveInjector_
であることがわかります.次に、IoCコンテナからクラスのインスタンスオブジェクトを取得するロジックを実装します.
class ReflectiveInjector_ implements ReflectiveInjector {
// ...
get(token: any): any {
return this._getByKey(ReflectiveKey.get(token))
}
private _getByKey(key: ReflectiveKey, isDeps?: boolean) {
for (let i = 0; i this.keyIds.length; i++) {
if (this.keyIds[i] === key.id) {
if (this.objs[i] === undefined) {
this.objs[i] = this._new(this._providers[i])
}
return this.objs[i]
}
}
let res = isDeps ? (key.token as Type).name : key.token
throw noProviderError(res)
}
_new(provider: ResolvedReflectiveProvider) {
const resolvedReflectiveFactory = provider.resolvedFactory
const factory = resolvedReflectiveFactory.factory
let deps = resolvedReflectiveFactory.dependencies.map(dep => this._getByKey(dep.key, true))
return factory(...deps)
}
}
injector.get()
メソッドを呼び出すと、IoCコンテナは、特定のクラスに基づいて対応するReflectiveInjector_
オブジェクトを検索し、見つけた後、特定のクラスをインスタンス化する前にクラス依存するすべてのクラスのインスタンスオブジェクトを注入し、最後に特定のクラスのインスタンスオブジェクトを返すことがわかります.ここで、上記のコードを振り返ると、マルチレベル依存の機能はすでに実現されています.loCコンテナを初期化すると、クラスの依存関係しか見つかりません.依存クラスによってより深い依存関係を見つけることはできませんが、提供されたクラスごとに同じ操作を実行しているので、自然に複数のクラス間の依存関係が実現されます.
エッジの場合も対応する処理を行いました.例えば、提供されたproviderクラスは空の配列であり、クラスは
@Injectable
装飾器によって修飾されておらず、提供されたクラスは不完全であるなどです.対応するコードは次のとおりです.let res = isDeps ? (key.token as Type).name : key.token
throw noProviderError(res)
if (!isInjectable) throw noAnnotationError(type)
これにより,制御反転と依存注入のコア機能との差は少なく,残りはインタフェース定義コード,さらに
ReflectiveKey
クラスの実装であり,その大まかな役割はES 6におけるMapストレージproviderクラスに基づいている.