← Курс/Parameters, ReturnType и другие типы функций#180 из 257+25 XP

Parameters, ReturnType и другие типы функций

ReturnType<T>

Извлекает тип, который возвращает функция:

function createUser(name: string, age: number) {
  return { id: Date.now(), name, age, active: true }
}

type UserType = ReturnType<typeof createUser>
// { id: number; name: string; age: number; active: boolean }

// Полезно когда у функции нет явной аннотации возвращаемого типа:
type ApiResponse = ReturnType<typeof fetchData>

// С async функциями ReturnType даёт Promise<T>:
async function loadUser(): Promise<User> { /* ... */ }
type LoadResult = ReturnType<typeof loadUser>       // Promise<User>
type UserData   = Awaited<ReturnType<typeof loadUser>> // User

Parameters<T>

Извлекает типы параметров как кортеж:

function greet(name: string, age: number, greeting: string): string {
  return `${greeting}, ${name}!`
}

type GreetParams = Parameters<typeof greet>
// [name: string, age: number, greeting: string]

// Получить тип конкретного параметра:
type FirstParam = Parameters<typeof greet>[0]  // string

// Практика: тип для функции-обёртки
function withLogging<F extends (...args: any[]) => any>(
  fn: F
): (...args: Parameters<F>) => ReturnType<F> {
  return (...args) => {
    console.log('Вызов с:', args)
    return fn(...args)
  }
}

ConstructorParameters<T>

Как Parameters, но для конструкторов классов:

class Database {
  constructor(
    private url: string,
    private port: number,
    private options?: { ssl: boolean }
  ) {}
}

type DBParams = ConstructorParameters<typeof Database>
// [url: string, port: number, options?: { ssl: boolean }]

// Фабрика через ConstructorParameters:
function createInstance<T extends new (...args: any[]) => any>(
  Ctor: T,
  ...args: ConstructorParameters<T>
): InstanceType<T> {
  return new Ctor(...args)
}

const db = createInstance(Database, 'localhost', 5432, { ssl: true })

InstanceType<T>

Тип, который создаёт конструктор:

class UserService {
  getUser(id: number) { return { id, name: 'Алексей' } }
}

type ServiceInstance = InstanceType<typeof UserService>
// UserService

// Полезно в DI-контейнерах:
type ServiceFactory<T extends new (...args: any[]) => any> = {
  create(): InstanceType<T>
  singleton: InstanceType<T> | null
}

Практические комбинации

// 1. Debounce с правильными типами:
function debounce<F extends (...args: any[]) => any>(
  fn: F,
  ms: number
): (...args: Parameters<F>) => void {
  let timer: ReturnType<typeof setTimeout>
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), ms)
  }
}

// 2. Memoize с правильными типами:
function memoize<F extends (...args: any[]) => any>(
  fn: F
): (...args: Parameters<F>) => ReturnType<F> {
  const cache = new Map<string, ReturnType<F>>()
  return (...args) => {
    const key = JSON.stringify(args)
    if (!cache.has(key)) cache.set(key, fn(...args))
    return cache.get(key)!
  }
}

// 3. Curry первого аргумента:
function partial<F extends (first: any, ...rest: any[]) => any>(
  fn: F,
  first: Parameters<F>[0]
): (...rest: Parameters<F> extends [any, ...infer R] ? R : never) => ReturnType<F> {
  return (...rest) => fn(first, ...rest)
}

Примеры

Функции высшего порядка с выводом типов: debounce, memoize, partial, compose — используем Parameters и ReturnType паттерны

// TypeScript: Parameters<F> и ReturnType<F> — извлекают типы параметров и результата.
// В JS реализуем те же паттерны — функции, которые работают с любыми функциями.

// debounce: откладывает вызов до тех пор пока не прекратятся вызовы
function debounce(fn, ms) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
      fn.apply(this, args)
    }, ms)
  }
}

// throttle: не чаще одного раза за ms миллисекунд
function throttle(fn, ms) {
  let lastTime = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastTime >= ms) {
      lastTime = now
      return fn.apply(this, args)
    }
  }
}

// memoize: кэширует результаты
function memoize(fn) {
  const cache = new Map()
  return function (...args) {
    const key = JSON.stringify(args)
    if (cache.has(key)) return cache.get(key)
    const result = fn.apply(this, args)
    cache.set(key, result)
    return result
  }
}

// partial: частичное применение первых N аргументов
function partial(fn, ...preArgs) {
  return function (...laterArgs) {
    return fn(...preArgs, ...laterArgs)
  }
}

// pipe: последовательная композиция функций
function pipe(...fns) {
  return function (value) {
    return fns.reduce((acc, fn) => fn(acc), value)
  }
}

// withRetry: повторяет синхронную функцию при ошибке
function withRetry(fn, attempts = 3) {
  return function (...args) {
    let lastError
    for (let i = 0; i < attempts; i++) {
      try {
        return fn(...args)
      } catch (e) {
        lastError = e
        console.log(`[retry] попытка ${i + 1} не удалась: ${e.message}`)
      }
    }
    throw lastError
  }
}

// --- Демонстрация ---

console.log('=== memoize (аналог Parameters<F>/ReturnType<F>) ===')
let fib_calls = 0
const fibonacci = memoize(function fib(n) {
  fib_calls++
  if (n <= 1) return n
  return fibonacci(n - 1) + fibonacci(n - 2)
})

console.log('fib(10):', fibonacci(10))   // 55
console.log('fib(10):', fibonacci(10))   // 55 (кэш)
console.log('fib(5):', fibonacci(5))     // 5 (кэш)
console.log('Уникальных вызовов:', fib_calls)

console.log('\n=== partial (ConstructorParameters pattern) ===')
function createRequest(baseUrl, endpoint, method, data) {
  return { url: baseUrl + endpoint, method, data }
}

// Фиксируем baseUrl — создаём специализированную функцию
const apiRequest = partial(createRequest, 'https://api.example.com')
console.log(apiRequest('/users', 'GET', null))
console.log(apiRequest('/posts', 'POST', { title: 'Hello' }))

// Фиксируем baseUrl + endpoint
const createUser = partial(createRequest, 'https://api.example.com', '/users', 'POST')
console.log(createUser({ name: 'Алексей' }))

console.log('\n=== pipe (compose) ===')
const processText = pipe(
  s => s.trim(),
  s => s.toLowerCase(),
  s => s.replace(/s+/g, '-'),
  s => `slug-${s}`
)

console.log(processText('  Hello World  '))   // 'slug-hello-world'
console.log(processText('TypeScript Types'))  // 'slug-typescript-types'

console.log('\n=== withRetry ===')
let attempt = 0
const unreliable = withRetry(function compute(x) {
  attempt++
  if (attempt < 3) throw new Error('Временная ошибка')
  return x * 2
}, 3)

try {
  const result = unreliable(21)
  console.log('Результат:', result)  // 42
} catch (e) {
  console.log('Не удалось:', e.message)
}

console.log('\n=== debounce (демонстрация) ===')
let searchCount = 0
const search = debounce((query) => {
  searchCount++
  console.log(`Поиск: "${query}" (вызов #${searchCount})`)
}, 100)

// Быстрые вызовы — только последний выполнится
search('T')
search('Ty')
search('Typ')
search('Type')  // только этот выполнится

setTimeout(() => {
  console.log('Поисков выполнено:', searchCount)  // 1
}, 200)