Ts 体操类型

前置知识

  • keyof 操作符:使用 keyof 操作符可以获取一个对象类型的所有 key 或 数字字面量组合的联合类型。
type Person = {
  name: string
  age: number
  heigh: number
  weigh: number
}

type Keys = keyof Person // 'name' | 'age' | 'heigh' | 'weigh'
  • typeof 操作符:使用 typeof 操作符可以获取一个值的类型。
type Person = {
  name: string
  age: number
  heigh: number
  weigh: number
}

type PersonType = typeof Person // { name: string, age: number, heigh: number, weigh: number }
  • 下标操作符:使用下标操作符可以获取一个类型的索引类型,使用关键字keyof
type Person = {
  name: string
  age: number
  heigh: number
  weigh: number
  isMain: boolean
}

type NameType = Person['name'] // string
// 获取一个对象的所有 key 的类型
type ValueType = Person[keyof Person] // string | number | boolean
  • 条件类型:使用条件类型可以根据一个类型的特性来确定另一个类型,在 ts 在可以通过 extends 关键字来实现条件类型。
type Person = {
  name: string
  age: number
  heigh: number
  weigh: number
}

type IsPerson<T> = T extends Person ? true : false
  • 约束条件类型:使用 extends 关键字可以约束一个类型的特性,例如约束一个类型必须是一个对象类型。
type Person = {
  name: string
  age: number
  heigh: number
  weigh: number
}

type IsObject<T> = T extends object ? true : false
type IsPerson = IsObject<Person> // true
  • 在条件类型中使用 infer 关键字进行类型推断操作: infer 关键字可以在条件类型中推断出一个类型。
// 获取返回值类型
type GetReturnType<T> = T extends (...args: never[]) => infer Return
  ? Return
  : never

type Fn = () => string

type FnReturnType = GetReturnType<Fn> // string
  • 映射类型:使用映射类型可以将一个类型的所有属性转换成另一个类型,使用关键字in
    • 在属性修饰符前添加 - 或者 + 前缀,再结合映射类型,可以实现对类型属性修饰符的修改操作。
    • - 操作符可以移除属性修饰符、+ 操作符可以实现添加属性修饰符操作
// 将一个类型的所有属性转换成另一个布尔类型
type TurnBoolean<T> = {
  [Key in keyof T]: boolean
}

type Person = {
  name: string
  age: number
  heigh: number
}

type PersonBoolean = TurnBoolean<Person> // { name: boolean, age: boolean, heigh: boolean }

type CreateMutable<T> = {
  -readonly [Property in keyof T]: T[Property]
}

type LockedAccount = {
  readonly id: string
  readonly name: string
}

type UnlockedAccount = CreateMutable<LockedAccount> // { id: string, name: string }

// === 移除属性修饰符 ===
type Concrete<T> = {
  [Property in keyof T]-?: T[Property]
}

type MaybeUser = {
  id: string
  name?: string
  age?: number
}

type User = Concrete<MaybeUser> // { id: string, name: string, age: number }
  • 映射类型结合 as 关键字实现对属性重命名: 使用 as 关键字可以实现对属性重命名操作。
type GettersPrefix<T> = {
    [Property in keyof T as `get_${string & Property}`]: () => T[Property]
};

type Person {
    name: string
    age: number
    heigh: number
}

type PersonGetters = GettersPrefix<Person> // { get_name: () => string, get_age: () => number, get_heigh: () => number }
  • 映射类型结合条件类型操作符: 映射类型可以结合条件类型实现一些更高阶的操作。下面 ExtractPII 实现了通过条件类型,更改特定属性值类型的操作:。
type ExtractPII<Type> = {
  [Property in keyof Type]: Type[Property] extends { pii: true } ? true : false
}

type DBFields = {
  id: { format: 'incrementing' }
  name: { type: string; pii: true }
}

type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields> // { id: false, name: true }
  • 模板字符串类型: 使用模板字符串类型可以实现对字符串的拼接操作。
type Person = { name: string; age: number; address: string }

// 全部重命名
type RenamePerson<T> = {
  [K in keyof T as `rename_${K & string}`]: T[K]
}
// 仅为指定的key重命名
type RenamePersonForKey<T, X extends keyof T> = {
  [K in keyof T as K extends X ? `rename_${K & string}` : K]: T[K]
}

type RenamedPerson = RenamePerson<Person> // { rename_name: string, rename_age: number, rename_address: string }
type RenamedPersonForKey = RenamePersonForKey<Person, 'name' | 'age'> // { rename_name: string, rename_age: number, address: string }

字符串类型体操

  • 字符串首字母大写 CapitalizeString
type CapitalizeString<S extends string> =
  S extends `${infer First}${infer Rest}` ? `${Uppercase<First>}${Rest}` : S

type res = CapitalizeString<'hello'> // 'Hello'
type res2 = CapitalizeString<'hello world'> // 'Hello world'
  • 获取字符串第一个字符 FirstChar
type FirstChar<S extends string> = S extends `${infer First}${infer Rest}`
  ? First
  : never

type res = FirstChar<'hello'> // 'h'
  • 获取字符串最后一个字符 LastChar
type LastChar<S extends string> = S extends `${infer First}${infer Rest}`
  ? LastChar<Rest>
  : First

type res = LastChar<'hello'> // 'o'
  • 字符串转元组 StringToTuple
type StringToTuple<S extends string> = S extends `${infer First}${infer Rest}`
  ? [First, ...StringToTuple<Rest>]
  : []

type res = StringToTuple<'hello'> // ['h', 'e', 'l', 'l', 'o']
  • 元组转字符串 TupleToString
type TupleToString<T extends any[]> = T extends [infer First, ...infer Rest]
  ? `${First}${TupleToString<Rest>}`
  : ''

type res = TupleToString<['h', 'e', 'l', 'l', 'o']> // 'hello'
  • 重复字符串 RepeatString
type RepeatString<S extends string, N extends number> = N extends 0
  ? ''
  : S extends `${infer First}${infer Rest}`
  ? `${First}${RepeatString<S, N>}`
  : ''

type res = RepeatString<'hello', 3> // 'hellohellohello'
  • 字符串分割 SplitString
type SplitString<
  S extends string,
  D extends string
> = S extends `${infer First}${D}${infer Rest}`
  ? [First, ...SplitString<Rest, D>]
  : [S]

type res = SplitString<'hello world', ' '> // ['hello', 'world']
  • 获取字符串长度 LengthOfString
type LengthOfString<S extends string> = S extends `${infer First}${infer Rest}`
  ? 1 + LengthOfString<Rest>
  : 0

type res = LengthOfString<'hello'> // 5
  • 驼峰转为短横线隔开式 KebabCase
type KebabCase<S extends string> = S extends `${infer First}${infer Rest}`
  ? First extends Uppercase<First>
    ? `-${Lowercase<First>}${KebabCase<Rest>}`
    : `${First}${KebabCase<Rest>}`
  : ''

type res = KebabCase<'helloWorld'> // 'hello-world'
  • 短横线隔开式转为驼峰 CamelCase

    type CamelCase<S extends string> = S extends `${infer First}${infer Rest}`
      ? First extends '-'
        ? `${Uppercase<Rest>}`
        : `${First}${CamelCase<Rest>}`
      : ''
    
    type res = CamelCase<'hello-world'> // 'helloWorld'
    
  • 字符串是否包含某个字符 Include

type Include<
  S extends string,
  C extends string
> = S extends `${infer First}${infer Rest}`
  ? First extends C
    ? true
    : Include<Rest, C>
  : false

type res = Include<'hello', 'e'> // true
  • 去掉左右空格 Trim
type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t'}${infer Rest}`
  ? TrimLeft<Rest>
  : S
type TrimRight<S extends string> = S extends `${infer Rest}${' ' | '\n' | '\t'}`
  ? TrimRight<Rest>
  : S

type Trim<S extends string> = TrimLeft<TrimRight<S>>

type res = Trim<' hello '> // 'hello'
  • 字符串替换 Replace
type Replace<
  T extends string,
  C extends string,
  RC extends string,
  F extends string = ''
> =
  // 空格替换 特殊处理
  C extends ''
    ? T extends ''
      ? RC
      : `${RC}${T}`
    : T extends `${infer L}${C}${infer R}` // 匹配模式
    ? Replace<R, C, RC, `${F}${L}${RC}`> // 结果拼接并替换
    : `${F}${T}`

type res = Replace<'hello', 'e', 'a'> // 'hallo'
  • 函数重命名改变返回值类型 ComponentEmitsType
// 转化为
/*
  {
      onHandleOpen?: (flag: boolean) => void,
      onPreviewItem?: (data: { item: any, index: number }) => void,
      onCloseItem?: (data: { item: any, index: number }) => void,
  }
  */
type a1 = {
  'handle-open': (flag: boolean) => true
  'preview-item': (data: { item: any; index: number }) => true
  'close-item': (data: { item: any; index: number }) => true
}

type CamelCase<
  T extends string,
  F extends string = ''
> = T extends `${infer L}-${infer R1}${infer R2}`
  ? CamelCase<R2, `${F}${L}${Capitalize<R1>}`> // 递归R2,去掉-,拼接大写的R1
  : Capitalize<`${F}${T}`> // 结果首字母也需要大写

type ComponentEmitsType<T> = {
  [K in keyof T as `on${CamelCase<K & string>}`]: T[K] extends (
    ...args: infer P
  ) => any // 参数类型不变
    ? (...args: P) => void // 仅改变返回值类型
    : T[K]
}

type a2 = ComponentEmitsType<a1>

type-challenges

开始

  • Hello World
type HelloWorld = string

// you should make this work
type test = Expect<Equal<HelloWorld, string>>

简单

  • Pick
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

// you should make this work
const todo: TodoPreview = {
  title: 'Clean room',
  completed: false,
}
  • Readonly
type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}

interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: 'Hey',
  description: 'foobar',
}

todo.title = 'Hello' // Error: cannot reassign a readonly property
todo.description = 'barFoo' // Error: cannot reassign a readonly property
  • Tuple to Object
type TupleToObject<T extends readonly any[]> = {
  [P in T[number]]: P
}
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
  • First of Array
type First<T extends any[]> = T extends [] ? never : T[0]

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
  • Length of Tuple
type Length<T extends readonly any[]> = T['length']

type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = [
  'FALCON 9',
  'FALCON HEAVY',
  'DRAGON',
  'STARSHIP',
  'HUMAN SPACEFLIGHT'
]

type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5
  • Exclude
type MyExclude<T, U> = T extends U ? never : T

type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
  • Awaited
type MyAwaited<T> = T extends Promise<infer R> ? R : never

type ExampleType = Promise<string>

type Result = MyAwaited<ExampleType> // string
  • If
type If<C extends boolean, T, F> = C extends true ? T : F

type A = If<true, 'a', 'b'> // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'
  • Concat
type Concat<T extends any[], U extends any[]> = [...T, ...U]
type Result = Concat<[1], [2]> // expected to be [1, 2]
  • Includes
type Includes<T extends readonly any[], U> = U extends T[number] ? true : false
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
  • Push
type Push<T extends any[], U> = [...T, U]
type Result = Push<[1, 2], '3'> // [1, 2, '3']
  • Unshift
type Unshift<T extends any[], U> = [U, ...T]
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
  • Parameters 获取函数参数类型
type MyParameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never

const foo = (arg1: string, arg2: number): void => {}

type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]

中级

  • Get Return Type 函数返回值
type MyReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : never

const fn = (v: boolean) => {
  if (v) return 1
  else return 2
}

type a = MyReturnType<typeof fn> // should be "1 | 2"
  • Omit 从对象中移除某些属性
type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}
interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {
  completed: false,
}
  • Readonly2
interface Todo {
  title: string
  description: string
  completed: boolean
}

type MyReadonly2<T, K extends keyof T = keyof T> = {
  readonly [P in K]: T[P]
} & {
  [P in Exclude<keyof T, K>]: T[P]
}

const todo: MyReadonly2<Todo, 'title' | 'description'> = {
  title: 'Hey',
  description: 'foobar',
  completed: false,
}

todo.title = 'Hello' // Error: cannot reassign a readonly property
todo.description = 'barFoo' // Error: cannot reassign a readonly property
todo.completed = true // OK
  • Deep Readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

type X = {
  x: {
    a: 1
    b: 'hi'
  }
  y: 'hey'
}

type Expected = {
  readonly x: {
    readonly a: 1
    readonly b: 'hi'
  }
  readonly y: 'hey'
}

type Todo = DeepReadonly<X> // should be same as `Expected`
  • Tuple to Union
type TupleToUnion<T extends any[]> = T[number]

type Arr = ['1', '2', '3']

type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'
  • Chainable Options
type Chainable<T = {}> = {
  option<K extends string, V>(key: K, value: V): Chainable<T & { [P in K]: V }>
  get(): T
}

declare const config: Chainable

const result = config
  .option('foo', 123)
  .option('name', 'type-challenges')
  .option('bar', { value: 'Hello World' })
  .get()

// expect the type of result to be:
interface Result {
  foo: number
  name: string
  bar: {
    value: string
  }
}

高级

极难

参考资料