preface
I’ve tried TypeScript for everyday coding for a long time, but I don’t know enough about statically typed languages and TypeScript’s type system is so complex that I feel I’m not taking advantage of the language’s ability to make code more readable and maintainable. So these days I want to study this language, hoping to sum up some special language features and practical skills.
The operator
Typeof – gets the typeof a variable
const colors = {
red: 'red',
blue: 'blue'
}
// type res = { red: string; blue: string }
type res = typeof colors
Copy the code
Keyof – gets the keyof the type
const data = {
a: 3,
hello: 'world'
}
// Type protection
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
Copy the code
Combines typeof with keyof – the name of the capture key
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
Copy the code
In – Traversal key name
interface Square {
kind: 'square'
size: number
}
// type res = (radius: number) => { kind: 'square'; size: number }
type res = (radius: number) = > { [T in keyof Square]: Square[T] }
Copy the code
Special type
Nested interface types
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}}]Copy the code
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'}}Copy the code
Conditions in the
type isBool<T> = T extends boolean ? true : false
// type t1 = false
type t1 = isBool<number>
// type t2 = true
type t2 = isBool<false>
Copy the code
A dictionary type
interface Dictionary<T> {
[index: string]: T
}
const data: Dictionary<number> = {
a: 3,
b: 4,}Copy the code
Infer – Delay inference type
type ParamType<T> = T extends (param: infer P) => any ? P : T
interface User {
name: string
age: number
}
type Func = (user: User) = > void
type Param = ParamType<Func> // Param = User
type AA = ParamType<string> // string
Copy the code
type ElementOf<T> = T extends Array<infer E> ? E : never
type TTuple = [string.number]
type ToUnion = ElementOf<TTuple> // string | number
Copy the code
The commonly used skill
Maintains a list of constants using const enum
const enum STATUS {
TODO = 'TODO',
DONE = 'DONE',
DOING = 'DOING'
}
function todos(status: STATUS) :Todo[] {
// ...
}
todos(STATUS.TODO)
Copy the code
Partial & Pick
type Partial<T> = {
[P inkeyof T]? : T[P] }type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
interface User {
id: number
age: number
name: string
}
// type PartialUser = { id? : number; age? : number; name? : string }
type PartialUser = Partial<User>
// type PickUser = { id: number; age: number }
type PickUser = Pick<User, 'id'|'age'>
Copy the code
Exclude & Omit
type Exclude<T, U> = T extends U ? never : T
// type A = 'a'
type A = Exclude<'x' | 'a'.'x' | 'y' | 'z'>
Copy the code
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
interface User {
id: number
age: number
name: string
}
// type PickUser = { age: number; name: string }
type OmitUser = Omit<User, 'id'>
Copy the code
Use the never type wisely
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
type NonFunctionPropertyNames<T> = { [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<Part> // 'updatePart'
type T41 = NonFunctionPropertyNames<Part> // 'id' | 'name' | 'subparts'
Copy the code
Mixins
// All mixins are required
type Constructor<T = {}> = new(... args:any[]) => T
// Add mixed examples of attributes
function TimesTamped<TBase extends Constructor> (Base: TBase) {
return class extends Base {
timestamp = Date.now()
}
}
// Add mixed examples of attributes and methods
function Activatable<TBase extends Constructor> (Base: TBase) {
return class extends Base {
isActivated = false
activate() {
this.isActivated = true
}
deactivate() {
this.isActivated = false}}}// Simple class
class User {
name = ' '
}
// Add User for TimesTamped
const TimestampedUser = TimesTamped(User)
// Add classes for TimesTamped and Activatable
const TimestampedActivatableUser = TimesTamped(Activatable(User))
// Use composite classes
const timestampedUserExample = new TimestampedUser()
console.log(timestampedUserExample.timestamp)
const timestampedActivatableUserExample = new TimestampedActivatableUser()
console.log(timestampedActivatableUserExample.timestamp)
console.log(timestampedActivatableUserExample.isActivated)
Copy the code
Type conversion
There is a class called EffectModule that implements the following:
interfaceAction<T> { payload? : Ttype: string
}
class EffectModule {
count = 1
message = 'hello! '
delay(input: Promise<number>) {
return input.then(i= > ({
payload: `hello ${i}! `.type: 'delay'
}))
}
setMessage(action: Action<Date>) {
return {
payload: action.payload.getMilliseconds(),
type: 'set-message'}}}Copy the code
Now there is a connect function that takes an instance of the EffectModule class as an argument and returns the new object. The implementation is as follows:
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<string>
setMessage(action: Date): Action<number>}const connected: Connected = connect(new EffectModule())
Copy the code
You can see that after calling connect, the new object returned contains only the method of the same name on the EffectModule, and the method’s type signature has changed:
asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>> becomes asyncMethod<T, U>(input: T): Action<U>Copy the code
SyncMethod <T, U>(action: action <T>): Action<U> becomes syncMethod<T, U>(action: T): action <U>Copy the code
Now we need to write type Connect = (Module: EffectModule) => any for the final compilation to pass. It is not difficult to see that this topic mainly examines two points:
- Select a function from a class
- Infer is skillfully used for type conversions
Here is my answer to the problem:
type FuncName<T> = { [P in keyof T]: T[P] extends Function ? P : never }[keyof T]
type Middle = { [T in FuncName<EffectModule>]: EffectModule[T] }
type Transfer<T> = {
[P in keyof T]: T[P] extends (input: Promise<infer J>) => Promise<infer K>
? (input: J) = > K
: T[P] extends (action: Action<infer J>) => infer K
? (input: J) = > K
: never
}
type Connect = (module: EffectModule) = > { [T in keyof Transfer<Middle>]: Transfer<Middle>[T] }
Copy the code
Inversion of control and dependency injection
Inversion of Control and Dependency Injection are very important ideas and principles in object-oriented programming. According to Wikipedia, IoC can reduce coupling between computer code, while DI stands for the process of injecting all the objects that an object depends on when it is created. Angular, the front-end framework, and Nest, the Node.js-based back-end framework, both reference this idea. The specific elaboration of these two concepts is not extended here, but readers can check out the two articles [1] [2]. Here we implement a simplified version of Angular inversion of control and Dependency Injection based on the old Angular 5 Dependency Injection.
First, let’s write a related test code:
import { expect } from 'chai'
import { Injectable, createInjector } from './injection'
class Engine {}
class DashboardSoftware {}
@Injectable(a)class Dashboard {
constructor(public software: DashboardSoftware) {}}@Injectable(a)class Car {
constructor(public engine: Engine) {}}@Injectable(a)class CarWithDashboard {
constructor(public engine: Engine, public dashboard: Dashboard) {}}class NoAnnotations {
constructor(_secretDependency: any) {}
}
describe('injector'.(a)= > {
it('should instantiate a class without dependencies'.(a)= > {
const injector = createInjector([Engine])
const engine = injector.get(Engine)
expect(engine instanceof Engine).to.be.true
})
it('should resolve dependencies based on type information'.(a)= > {
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'.(a)= > {
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'.(a)= > {
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'.(a)= > {
const injector = createInjector([CarWithDashboard, Engine, Dashboard])
expect((a)= > injector.get(CarWithDashboard)).to.throw('No provider for DashboardSoftware! ')
})
it('should throw when no type'.(a)= > {
expect((a)= > createInjector([NoAnnotations])).to.throw(
'Make sure that NoAnnotations is decorated with Injectable.'
)
})
it('should throw when no provider defined'.(a)= > {
const injector = createInjector([])
expect((a)= > injector.get('NonExisting')).to.throw('No provider for NonExisting! ')})})Copy the code
We can see that there are three core functions we want to implement:
- Create IoC containers from the provided classes and be able to manage dependencies between classes
- The associated dependencies are injected when instance objects of the class are retrieved through the IoC container
- Implement multi-level dependencies and handle edge cases
Let’s start with the simplest @Injectable decorator:
export const Injectable = (): ClassDecorator= > target => {
Reflect.defineMetadata('Injectable'.true, target)
}
Copy the code
Then we implement creating an IoC container that can manage dependencies between classes based on the provided provider class:
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 < len; 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<T>) : (args: any[]) = >T {
return (. args:any[]) = > newt(... args) }function dependenciesFor(type: Type<any>) :ReflectiveDependency[] {
const params = parameters(type)
return params.map(extractToken)
}
function parameters(type: Type<any>) {
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_
}
Copy the code
It is not hard to see from the above code will provide when the IoC container to create each class and the class relies on other classes as ResolvedReflectiveProvider_ instance of the object is stored in the container, ReflectiveInjector_ foreign return is a container object.
Let’s implement the logic to get an instance object of the class from the IoC container:
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))
returnfactory(... deps) } }Copy the code
Can see when we call injector. The get () method when the IoC container will consult the corresponding ResolvedReflectiveProvider_ object, according to the given class after finding will be instantiated in a given class into the class depends on all the classes before the instance of the object, Finally, the instantiated object of the given class is returned.
Now looking back at the code above, the multilevel dependencies are already in place. Although we can only find dependencies for one class when initializing the loC container, we cannot find deeper dependencies by relying on the class, but we perform the same operation for each provided class traversal, so it is natural to implement dependencies between multiple classes.
We also handled edge cases, such as providing an empty array for the provider class, not being decorated with the @Injectable decorator, and providing incomplete classes. The corresponding code above is:
let res = isDeps ? (key.token as Type).name : key.token
throw noProviderError(res)
Copy the code
if(! isInjectable)throw noAnnotationError(type)
Copy the code
At this point, the core functions of inversion of control and dependency injection are almost complete. What remains is some interface definition code and the implementation of the ReflectiveKey class, which basically stores the Provider class based on the Map in ES6. Interested readers can take a look at the full code sample.
Reference content:
Understand TypeScript in depth
TypeScript advanced techniques
About Dependency Injection
IoC & DI
Dependency Injection