Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
В чем разница между T Extends и T =?
Контекст: Generics в TypeScript
Это вопрос о работе с Generic типами в TypeScript. Обе конструкции используются в объявлении типов, но они имеют совершенно разные смыслы.
T Extends - ограничение типа (Constraint)
T extends U означает: "T должен быть подтипом U" или "T совместим с U".
// T может быть только строкой
function processString<T extends string>(value: T): T {
return value.toUpperCase() as T;
}
processString('hello'); // OK
processString(42); // ERROR: Type 'number' is not assignable to type 'string'
// T может быть любым объектом с свойством name
function getName<T extends { name: string }>(obj: T): string {
return obj.name;
}
getName({ name: 'John', age: 30 }); // OK
getName({ age: 30 }); // ERROR: Property 'name' is missing
// T может быть ключом объекта K
function getProperty<K extends string, T extends Record<K, any>>(
obj: T,
key: K
): T[K] {
return obj[key];
}
const user = { name: 'John', age: 30 };
getProperty(user, 'name'); // OK, возвращает string
getProperty(user, 'email'); // ERROR: email не существует
T = - значение по умолчанию (Default Type)
T = U означает: "если T не указан, используй U как значение по умолчанию".
// Если тип не указан, T будет string
function process<T = string>(value: T): T {
return value;
}
process('hello'); // T = string (вывод типа)
process<number>(42); // T = number (явное указание)
process(); // ERROR: нужен аргумент
// Generic с дефолтным значением
type Response<T = unknown> = {
data: T;
status: number;
};
const res1: Response = { data: undefined, status: 200 };
const res2: Response<string> = { data: 'hello', status: 200 };
Различия в таблице
| Концепция | T extends U | T = U |
|---|---|---|
| Смысл | Ограничение типа | Значение по умолчанию |
| Проверка | TypeScript проверяет соответствие | TypeScript использует U, если T не указан |
| Использование | Ограничивает возможные типы | Предоставляет fallback тип |
| Ошибка | При передаче неподходящего типа | Если дефолт тоже не подходит |
| Где писать | В объявлении generic | В объявлении generic |
Примеры различий
Пример 1: Простой случай
// T extends string - T ДОЛЖЕН быть string
function upper<T extends string>(str: T): T {
return str.toUpperCase() as T;
}
upper('hello'); // OK
upper(123); // ERROR: Type 'number' is not assignable
// T = string - если T не указан, используй string
function lower<T = string>(value: T): T {
return value as any; // T может быть чем угодно
}
lower('hello'); // T = string
lower(123); // T = number (OK!)
lower(); // ERROR: нужен аргумент
Пример 2: С объектами
// extends - ограничение структуры
function processUser<T extends { id: number; name: string }>(user: T) {
console.log(user.id, user.name);
}
processUser({ id: 1, name: 'John' }); // OK
processUser({ id: 1, name: 'John', age: 30 }); // OK (больше свойств)
processUser({ id: 1 }); // ERROR: name отсутствует
// = дефолт - используется если тип не указан
type User = { id: number; name: string };
function process<T = User>(data: T): T {
return data;
}
process({ id: 1, name: 'John' }); // T = User
process<{ email: string }>({ email: 'test' }); // T = { email: string }
Комбинирование extends и =
Можно использовать оба одновременно!
// T должен быть extends string, но по умолчанию = 'default'
function create<T extends string = 'default'>(value?: T): T {
return (value || 'default') as T;
}
create(); // T = 'default' (используется дефолт)
create('custom'); // T = 'custom'
create(123); // ERROR: number не extends string
// K должен быть extends keyof T, и по умолчанию = keyof T
function getProperty<T, K extends keyof T = keyof T>(obj: T, key?: K) {
return key ? obj[key] : null;
}
const user = { name: 'John', age: 30 };
getProperty(user); // K = keyof user
getProperty(user, 'name'); // K = 'name'
getProperty(user, 'invalid'); // ERROR
Реальные примеры из практики
Пример 1: Hook с типом данных
// Используем оба: ограничение И дефолт
function useApi<T extends object = Record<string, any>>(
url: string
): Promise<T> {
return fetch(url).then(r => r.json());
}
// Без явного типа - используется дефолт
const data1 = await useApi('/api/users');
// data1: Record<string, any>
// С явным типом
interface User { id: number; name: string; }
const data2 = await useApi<User>('/api/users/1');
// data2: User
Пример 2: Реакт компонент
// Дефолтный тип для props
function Button<T extends { onClick: () => void } = { onClick: () => {} }>(
props: T
) {
return <button onClick={props.onClick}>Click</button>;
}
const btn1 = <Button />; // T = { onClick: () => void }
const btn2 = <Button onClick={() => alert('hi')} />; // T = { onClick: () => void }
Пример 3: API wrapper
// ApiResponse может быть любого типа, по умолчанию null
class ApiClient<ResponseType = null> {
async get<T = ResponseType>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
}
const client = new ApiClient();
const data1 = await client.get('/api/users');
// data1: null (дефолт)
const data2 = await client.get<User>('/api/users/1');
// data2: User
Ошибки при неправильном использовании
// ОШИБКА: extends и = перепутаны
function wrong<T extends = string>(value: T) {}
// ^^ SyntaxError
// ПРАВИЛЬНО: сначала extends, потом =
function correct<T extends string = 'hello'>(value: T) {}
Визуализация
T extends string
├─ Проверка: "T совместим со string?"
├─ Передано 'hello' -> OK (string)
└─ Передано 123 -> ERROR (number не extends string)
T = string
├─ Вопрос: "T указан?"
├─ Да -> используй переданный тип
└─ Нет -> используй string как дефолт
Ключевые выводы
- T extends U - "T ДОЛЖЕН быть совместим с U" - это ОГРАНИЧЕНИЕ
- T = U - "если T не указан, используй U" - это ДЕФОЛТ
- Оба работают вместе:
<T extends string = 'default'>- T ограничен string, дефолт 'default' - extends проверяется при КОМПИЛЯЦИИ
- = дефолт применяется при ИСПОЛЬЗОВАНИИ без явного типа
- Порядок важен: сначала
extends, потом=