Skip to content

Instantly share code, notes, and snippets.

@buhichan
Last active October 19, 2020 11:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save buhichan/6793975c7b38aacff766e2379b7dcc93 to your computer and use it in GitHub Desktop.
Save buhichan/6793975c7b38aacff766e2379b7dcc93 to your computer and use it in GitHub Desktop.
A typescript DI solution with no need of reflect-metadata (and can also need no decorators, if you prefer it)
//Idea is borrowed from https://github.com/gnaeus/react-ioc
//Differences are:
//1. Does not require reflect-metadata
//2. Has an additional "useProvider" method
import * as React from "react"
export interface Disposable {
dispose?(): void
}
type SID<T = unknown> = string & {
interface: T
}
type ServiceFromServiceId<S> = S extends SID<infer T> ? T : never
export function createServiceId<T>(name: string): SID<T> {
return (name as unknown) as SID<T>
}
class InjectorResolutionError extends Error {
public cause = "Injector"
constructor() {
super("找不到Injector, 请确保被注入的目标类不是手动创建的.")
}
}
class ServiceResolutionError extends Error {
constructor(public cause: SID) {
super(`未能解析的服务: ${cause.toString()}`)
}
}
const CURRENT_INJECTOR = Symbol("CurrentInjector")
class Injector {
static ServiceResolutionError = ServiceResolutionError
private instanceMap: Record<SID, Disposable> & object = {}
private bindingMap: Record<SID, new () => Disposable> & object = {}
constructor(private parent?: Injector) {}
dispose() {
for(let k in this.instanceMap){
this.instanceMap[k].dispose()
}
}
register(serviceId: SID, impl: new () => Disposable) {
this.bindingMap[serviceId] = impl
}
get<T extends Disposable>(serviceId: SID): T {
if (this.bindingMap[serviceId]) {
if (this.instanceMap[serviceId]) {
return this.instanceMap[serviceId] as T
}
const res = new this.bindingMap[serviceId]()
res[CURRENT_INJECTOR] = this
this.instanceMap[serviceId] = res
return res as T
} else {
if (this.parent) {
return this.parent.get(serviceId)
} else {
throw new Injector.ServiceResolutionError(serviceId)
}
}
}
}
const rootInjector = new Injector()
const InjectorContext = React.createContext(rootInjector)
export type Declaration<T> = [SID<T>, new ()=>T]
export function useProvider<T1>(d1: Declaration<T1>): (node: React.ReactNode) => React.ReactElement
export function useProvider<T1, T2>(d1: Declaration<T1>, d2:Declaration<T2>): (node: React.ReactNode) => React.ReactElement
export function useProvider<T1, T2, T3>(d1: Declaration<T1>, d2:Declaration<T2>, d3: Declaration<T3>): (node: React.ReactNode) => React.ReactElement
//eslint-disable-next-line
export function useProvider(...declarations: any[]): (node: React.ReactNode) => React.ReactElement {
const parent = React.useContext(InjectorContext)
const childInjector = React.useMemo(
() => {
const childInjector = new Injector(parent)
declarations.forEach(([id, impl]) => childInjector.register(id as SID, impl as new () => Disposable))
return childInjector
},
declarations.map(x => x[1])
)
React.useEffect(
() => () => {
childInjector.dispose()
},
[]
)
return node => {
return <InjectorContext.Provider value={childInjector}>{node}</InjectorContext.Provider>
}
}
export function Inject<Id extends SID>(serviceId: Id, target: unknown): ServiceFromServiceId<Id>
export function Inject<Id extends SID>(serviceId: Id): (proto: unknown, propName: string) => void
export function Inject<Id extends SID>(serviceId: Id, target?: unknown) {
if (target === undefined) {
return (proto, propName) => {
Object.defineProperty(proto, propName, {
get() {
const injector = this[CURRENT_INJECTOR]
if (!injector) {
throw new InjectorResolutionError()
}
console.log("get", serviceId)
return (injector.get(serviceId) as unknown) as ServiceFromServiceId<Id>
},
})
}
} else {
const injector = ((target as unknown) as Record<typeof CURRENT_INJECTOR, Injector>)[CURRENT_INJECTOR]
if (!injector) {
throw new InjectorResolutionError()
}
return (injector.get(serviceId) as unknown) as ServiceFromServiceId<Id>
}
}
export function useService<Id extends SID>(serviceId: Id) {
const injector = React.useContext(InjectorContext)
console.log("get ", serviceId)
return injector.get(serviceId) as ServiceFromServiceId<Id>
}
@buhichan
Copy link
Author

buhichan commented Oct 19, 2020

Then in some other files:

import { Inject, createServiceId, useProvider, Disposable, useService } from "./service"
import * as React from "react"

interface IAuth {
    username():string
}

const IAuth = createServiceId<IAuth>("Auth")

class AuthService implements IAuth, Disposable {
    username(){
        return "world"
    }
    dispose(){
        console.log("AuthService diposed")
    }
}

interface IUser {
    yell():string
}

class UserService {
    @Inject(IAuth)
    Auth: IAuth
    yell(){
        return "hello " + this.Auth.username()
    }
    dispose(){
        console.log("UserService diposed")
    }
}

class UserServiceAlternative {
    @Inject(IAuth)
    Auth: IAuth
    yell(){
        return "hello or not ? " + this.Auth.username()
    }
    dispose(){
        console.log("UserServiceAlternative diposed")
    }
}

const IUser = createServiceId<IUser>("IUser")

export function App(){

    const [show,setShow] = React.useState(false)
    const [useAltUserService, setUseAltUserService] = React.useState(false)

    const provider = useProvider(
        [IAuth, AuthService],
        [IUser,useAltUserService ? UserServiceAlternative : UserService]
    )

    return provider(<div>
        <button onClick={()=>{
            setShow(!show)
        }}>show child</button>
        <button onClick={()=>{
            setUseAltUserService(!useAltUserService)
        }}>switch service</button>
        {show && <Child></Child> }
    </div>) 
}

function Child(){

    const userService = useService(IUser)

    const provider = useProvider(
        [IUser, UserService2]
    )

    return provider(<div>
        child says {userService.yell()}
        <Child2></Child2>
    </div>)
}



class UserService2 {
    @Inject(IAuth)
    Auth: IAuth
    yell(){
        return "fuck " + this.Auth.username()
    }
    dispose(){
        console.log("UserService2 diposed")
    }
}

function Child2(){



    const userService = useService(IUser)

    return <div>
        child2 says {userService.yell()}
    </div>
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment