TypeScript 类型 tips

 

从对象类型衍生一个 union 类型

export const fruitCounts = {
    apple: 1,
    pear: 4,
    banana: 26,
}

type FruitCounts = typeof fruitCounts

/*
type SingleFruitCount =
  | {
      apple: number
    }
  | {
      banana: number
    }
  | {
      pear: number
    }
*/

type SingleFruitCount = {
    [K in keyof FruitCounts]: {
        [k2 in K]: FruitCounts[K]
    }
}[keyof FruitCounts]


const singleFruitCount: SingleFruitCount = {
    apple: 2
}

使用 in 操作符转换一个 union 为另一个 union

export type Entity =
  | {
      type: "user"
    }
  | {
      type: "post"
    }
  | {
      type: "comment"
    }

/**
	type EntityWithId = ({
    type: "user";
} & Record<"userId", string>) | ({
    type: "post";
} & Record<"postId", string>) | ({
    type: "comment";
} & Record<"commentId", string>)
*/

type EntityWithId = {
  [EntityType in Entity["type"]]: {
    type: EntityType
  } & Record<`${EntityType}Id`, string>
}[Entity["type"]]

const result: EntityWithId = {
    type: 'comment',
    commentId: '132',
}

使用 extends 关键字推测泛型的值

export const getDeepValue = <
    Obj,
    FirstKey extends keyof Obj,
    SecondKey extends keyof Obj[FirstKey]
>(
    obj: Obj,
    firstKey: FirstKey,
    secondKey: SecondKey
)=> {
    return obj[firstKey][secondKey]
}

const obj = {
    foo: {
        a: true,
        b: 2
    },
    bar: {
        c: 'cool',
        d: 2,
    }
}

// result: number
const result = getDeepValue(obj, "bar", "d")

接收任意 React 组件的 Props 类型

import React from 'react'

const MyComponent = (props: {enabled: boolean}) => {
  return null
}

class MyOtherComponent extends React.Component<{
  enabled: boolean
}>{}

type PropsFrom<T> = T extends React.FC<infer Props> ? Props : 
T extends React.Component<infer Props> ? Props : never

const props: PropsFrom<typeof MyComponent> = {
	enabled: true,
}

使用泛型创建动态 React 组件

import React from 'react'

interface TableProps<T> {
  items: T[]
  renderItem: (item: T) => React.ReactNode
}

export const Table = <T,>(props: TableProps<T>) => {
  return null
}

const Component = () => {
  return (
  	<Table<{id: string}>
      items={[
        {
          id: '1',
        }
      ]}
      renderItem={(item) => <div>{item.id}</div>}
      >
    </Table>
  )
}

union 小技巧

// IconSize: string
type Size = "sm" | "xs" | string 

// type IconSize = "sm" | "xs" | Omit<string, "xs", "sm">  
type Size2 = "sm" | "xs" | Omit<string, "xs", "sm"> 

type LooseAutocomplete<T extends string> = T | Omit<string, T> 

使用泛型动态定义函数的参数类型

export type Event =
    | {
        type: "LOG_IN"
        payload: {
            userId: string
        }
    }
    | {
        type: "SIGN_OUT"
    }

const sendEvent = <Type extends Event["type"]>(
    ...args: Extract<Event, { type: Type }> extends { payload: infer TPayload }
        ? [Type, TPayload]
        : [Type]
) => { }

sendEvent('LOG_IN', {
    userId: "1",
})

sendEvent("SIGN_OUT")

遍历 union

export type Letters = "a" | "b" | "c"

type RemoveC<TType> = any

// type WowWithoutC = "a" | "b"
type WowWithoutC = RemoveC<Letters>

使用一个 “本地” 泛型来 “保存” 临时类型

type ValuesOfKeysStartingWithA<
    Obj,
    _ExtractedKeys extends keyof Obj = Extract<keyof Obj, `a${string}`>
> = {
    [K in _ExtractedKeys]: Obj[K]
}[_ExtractedKeys]

export type Obj = {
  a: "a"
  a2: "a2"
  a3: "a3"
  b: "b"
  b1: "b1"
  b2: "b2"
}

// type NewUnion = "a" | "a2" | "a3"
type NewUnion = ValuesOfKeysStartingWithA<Obj>

assertion function 之 class

export class SDK1 {
  constructor(public loggedInUserId?: string) {}

  createPost(title: string) {
    this.assertUserIsLoggedIn()

    // this.loggedInUserId: string | undefined  // *
    this.loggedInUserId
    this.createPost(title)
  }

  assertUserIsLoggedIn() {
    if (this.loggedInUserId) {
      throw new Error("User is not logged in")
    }
  }
}


export class SDK2 {
    constructor(public loggedInUserId?: string) { }

    createPost(title: string) {
        this.assertUserIsLoggedIn()
				
      	// this.loggedInUserId: string // *
        this.loggedInUserId
        this.createPost(title)
    }

    assertUserIsLoggedIn(): asserts this is this & { // *
        loggedInUserId: string
    } {
        if (this.loggedInUserId) {
            throw new Error("User is not logged in")
        }
    }
}

使用 infer 与字符串字面量操作对象的成员

interface ApiData {
    "maps:longitude": string
    "maps:latitude": string
    awesome: boolean
}

type RemoveMaps<T> = T extends `maps:${infer U}` ? never : T

type RemoveMapsFromObj<T> = {
    [K in keyof T as RemoveMaps<K>]: T[K]
}

/*
type DesiredShape = {
    awesome: boolean;
}
*/
type DesiredShape = RemoveMapsFromObj<ApiData>