preface

I have been reading TS recently. Many articles I read are about some concepts, without any practical operation or specific scenarios. That’s not to say that concepts aren’t important. There is a gap between knowing and not knowing, but there is also a gap between knowing and how to use, so I summed up some experience learned in this month into an article, specifically around some scenarios for thinking.

example

Scenario 1. The union type is converted to the crossover type

Let’s say we want to upload a video, and we have a 320p, 480p, 720p, 1080P structure based on the resolution that looks like this.

type Format320 = { urls: { format320p: string}}type Format480 = { urls: { format480p: string}}type Format720 = { urls: { format720p: string}}type Format1080 = { urls: { format1080p: string}}// Video type
type Video = Format1080 | Format720 | Format480 | Format320
Copy the code

At this time, if we want to type was derived by VideoType: format320p | format480p | format720p | format1080p. How do you do that?

One might say that it’s as simple as keyof Video[‘urls’], but it doesn’t work. You get never.

Why is that? Since Video is an associative type, Video[‘urls’] is also an associative type, they don’t have the same key, so it’s never

/** Video['urls'] = | { format320p: string } | { format480p: string } | { format720p: string } | { format1080p: string } */
type VideoType = keyof Video['urls']  // never
Copy the code

So how do we get the union of the keys that we want, let’s go back to keyof and under what circumstances can we get the VideoType that we want

type VideoType = keyof {
    format320p: string
    format480p: string
    format720p: string
    format1080p: string
} // ok
Copy the code

Ok, that’s ok. Let’s go a little further

type Urls = 
    & { format320p: string} and {format480p: string} and {format720p: string} and {format1080p: string }
type VideoType = keyof Urls // ok
Copy the code

Wow, great! At this point we’re halfway there, and it’s clear that compared to the previous Video[‘urls’], we only need to convert the federation type to the crossover type.

Let’s just give you the conversion method.

type UnionToIntersection<T> = 
    (
    T extends any 
        ? (any: T) = > any 
        : never
    )
    extends (any: infer Q) => any
        ? Q
        : never
  
Copy the code

To understand this code we need two preconditions:

  1. Distributed condition type
  2. Covariant, contravariant, double variant, invariant. This is a question aboutThe catUnder what circumstancesragdollPhilosophical problems of the parent class – -!!

For those who do not understand these two concepts, please read here first, search relevant articles and lay a solid foundation, and then come back to continue customs clearance!

Ok, so let’s move on. The key to this type is the second condition. Due to infer infer Q is parameters, is located in the inverter, TS for compatibility | will be converted to &. The first condition is extends any. All types can be converted from the union type to (any: T) => any, which is inferred by infer Q, which is also a infer parameter.

The complete code is as follows.

type Format320 = { urls: { format320p: string}}type Format480 = { urls: { format480p: string}}type Format720 = { urls: { format720p: string}}type Format1080 = { urls: { format1080p: string}}// Video type
type Video = Format1080 | Format720 | Format480 | Format320

type UnionToIntersection<T> = 
    (T extends any ? (arg: T) = > any : never) 
        extends (arg: infer Q) => any
            ? Q
            : never
            
type VideoType = keyof UnionToIntersection<Video['urls'] >Copy the code

Scenario 2. HasOwnProperty

So let’s look at a function like this

function print(person: object) {
    if (typeof person === 'object'  
        && person.hasOwnProperty('name')) {console.log(person.name) // Error, object does not have name attribute}}Copy the code

This is the classic case of “I know, but TS doesn’t know”, although we use hasOwnProperty to determine whether there is a name property, but TS doesn’t know, so what should we do?

Here, we need two concepts, one is a type predicate and the other is a crossover type.

Ok, let’s write a function that determines if the argument is object, and if so, retype the argument by crossing types.

function hasOwnProperty<
    Obj extends object.Key extends PropertyKey> (obj: Obj, key: Key) :obj is Obj & Record<Key.unknow> {
    return obj.hasOwnProperty(key)
}
Copy the code

The Key here is obj is obj & Record

. Since obj is obj is always true, TS allows us to retype the type when the result is true. Here we add the corresponding Key attribute for the type by crossing the types. So you don’t get any errors in the values that follow. Fett ~
,>

The complete code

function hasOwnProperty<
    Obj extends object.Key extends PropertyKey> (obj: Obj, key: Key) :obj is Obj & Record<Key.unknow> {
    return obj.hasOwnProperty(key)
}

function print(person: object) {
    if (typeof person === 'object'  
        && hasOwnProperty(person, 'name')
        // Here the person type becomes object & {name: unknown}
        && typeof person.name === 'string'
    ) {
        console.log(person.name) // ok}}Copy the code

DefineProperty

When we define an Object and assign a value to it via object.defineProperty, TS doesn’t know.

let storage = {
    maNumber: 99
}
Object.defineProperty(storage, 'number', {
    configurable: true.writable: true.enumberable: true.value: 10,})console.log(storage.number) / / the error! There is no number attribute
Copy the code

If this is a familiar scenario, as in scenario 2, we still need to retype the type with the type predicate.

However, unlike scenario 2, this time we need to consider some error cases, and here the preknowledge we need is the results statement.

Let’s first implement the defineProperty method

function defineProperty<
    Obj extends object.Key extends PropertyKey.PDesc extends PropertyDescriptor> (obj: Obj, key: Key, desc: PDesc) :asserts obj is Obj & DefineProperty<Key.PDesc> {
    Object.definePropety(obj, key, desc)
}
Copy the code

Ok, same pattern, predicate with a type that must be true, then cross type. Now all we have to do is implement DefineProperty.

So why do we use aseerts? We know that there are two modes of value, one is the value mode, the other is the accessor getter/setter, and if we define both in descriptor, JS is going to make an error, so we’re going to pre-detect that by TS.

So let’s implement DefineProperty

type DefineProperty<
    Key extends PropertyKey,
    Desc extends PropertyDescriptor> = 
    Desc extends { writable: any, set(val: any) :any}?never :
    Desc extends { writable: any, get(): any}?never :
    Desc extends { writable: false}? ReadOnly<InferValue<Key, Desc>> : Descextends { writable: true}? InferValue<Key, Desc> : ReadOnly<InferValue<Key, Desc>>Copy the code

InferValue: InferValue: InferValue: InferValue: InferValue: InferValue: InferValue: InferValue

Let’s go back and see what we want DefineProperty to return. From our experience in Scenario 2, we need a Record

. And since values have two types, we can infer types from InferValue.
,>

type InferValue<
    Key extends PropertyKey,
    Desc extends PropertyDescriptor> = 
    Desc extends { value: any, get(): any}?never :
    Desc extends { value: infer T } ? Record<Key, T> :
    Desc extends { get(): infer T } ? Record<Key, T> :
    never
Copy the code

And we’re done! The complete code is as follows

function defineProperty<
    Obj extends object.Key extends PropertyKey.PDesc extends PropertyDescriptor> (obj: Obj, key: Key, desc: PDesc) :asserts obj is Obj & DefineProperty<Key.PDesc> {
    Object.definePropety(obj, key, desc)
}

type DefineProperty<
    Key extends PropertyKey,
    Desc extends PropertyDescriptor> = 
    Desc extends { writable: any, set(val: any) :any}?never :
    Desc extends { writable: any, get(): any}?never :
    Desc extends { writable: false}? ReadOnly<InferValue<Key, Desc>> : Descextends { writable: true}? InferValue<Key, Desc> : ReadOnly<InferValue<Key, Desc>>type InferValue<
    Key extends PropertyKey,
    Desc extends PropertyDescriptor> = 
    Desc extends { value: any, get(): any}?never :
    Desc extends { value: infer T } ? Record<Key, T> :
    Desc extends { get(): infer T } ? Record<Key, T> :
    never

let storage = { maxValue: 20 }

defineProperty(storage, 'number'.123)

console.log(storage.number) // ok

Copy the code

exercises

Finally, to give you a classic exercise, SSH Haoshen also sent for this problem solution ideas, I put the answer in the bottom, you can first try to try, see if you can write out.

/** * declare function dispatch(arg: Action): void dispatch({ type: 'LOGIN', emialAddress: String}) to Dispatch ('LOGIN', {emialAddress: string}) */
type Action = 
  | {
    type: 'INIT'
  }
  | {
    type: 'SYNC'
  }
  | {
    type: 'LOG_IN'.emialAddress: string
  }
  | {
    type: 'LOG_IN-SUCCESS'.accessToken: string
  }
Copy the code

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

The answer

type Action = 
  | {
    type: 'INIT'
  }
  | {
    type: 'SYNC'
  }
  | {
    type: 'LOG_IN'.emialAddress: string
  }
  | {
    type: 'LOG_IN-SUCCESS'.accessToken: string
  }
declare function dispatch<T> (
    type: T,
    action: 
) :void
type ActionType = Action['type'] // Find correspondingtype
type ExtraAction<A.T> = A extends {type: T} ? A : never
// Remove the type attribute
type ExcludeTypeField<A> = {[K in Exclude<keyof A, 'type'>]: A[K]}
// Put together
type ExtractActionParameterWithoutType<A, T> = ExcludeTypeField<ExtraAction<A, T>>

type ExtractSimpleAction<T> = T extends any 
    ? {} extends ExcludeTypeField<T>
        ? T
        : never
    : never
type SimpleActionType = ExtractSimpleAction<Action>['type']
type ComplexActionType = Exclude<ActionType, SimpleActionType>

/ / overloaded
declare function dispatch<T extends SimpleActionType> (type: T) :void
declare function dispatch<T extends ComplexActionType> (
    type: T, 
    action: ExtractActionParameterWithoutType<Action, T>
) :void
Copy the code