Реализация утилитарного типа в TypeScript для сохранения нулабельности
Пошаговое руководство по созданию утилитарного типа в TypeScript для сохранения нулабельности без использования as any. Примеры кода и лучшие практики.
Как правильно реализовать утилитарный тип в TypeScript для сохранения нулабельности? У меня есть следующий код:
// Утилитарный тип для сохранения нулабельности
type NullabilityOf<T, U> = T extends null | undefined
? U | Extract<T, null | undefined>
: U;
interface Photo {
id: string;
}
export class PhotoHelper {
constructor(private image: Photo) { }
static from<T extends Photo | null | undefined>(
image: T
): NullabilityOf<T, PhotoHelper> {
if (image == null) {
return null;
}
return new PhotoHelper(image);
}
get displayUri(): string {
return `uri_for_photo_${this.image.id}`;
}
}
let a = PhotoHelper.from(null)
let b = PhotoHelper.from({id:"phoo"})
let pic: Photo | null = new Date().getTime() === 0 ? {id:"photo1"} : null
let c = PhotoHelper.from(pic)
Цель - чтобы функция корректно выводила нулабельность параметра и делала тип возвращаемого значения соответствующим ей. Таким образом, я получаю безопасность типов, делая:
console.log(a?.displayUri)
console.log(b.displayUri)
console.log(c?.displayUri)
Использование работает, но в определении есть проблемы:
Тип ‘null’ не присваивается типу ‘NullabilityOf<T, PhotoHelper>’.
Я нашел способ обойти это с помощью as any, но не хочу его использовать:
static from<T extends Photo | null | undefined>(
image: T
): NullabilityOf<T, PhotoHelper> {
if (image == null) {
return null as any;
}
return new PhotoHelper(image) as any;
}
Существуют ли в TypeScript новые, предстоящие или предложенные функции, которые могли бы сделать это чище и не требовать использования as any? Как правильно реализовать сохранение нулабельности в TypeScript без использования as any?
Вам нужен правильный подход к сохранению нулабельности в TypeScript без использования as any. Проблема заключается в том, что TypeScript не может автоматически вывести, что возвращение null допустимо, когда входной тип включает null или undefined.
Содержание
- Понимание проблемы сохранения нулабельности в TypeScript
- Встроенные утилитарные типы TypeScript для работы с null/undefined
- Реализация пользовательского утилитарного типа для сохранения нулабельности
- Альтернативные подходы без использования
as any - Лучшие практики работы с нулабельностью в TypeScript
Понимание проблемы сохранения нулабельности в TypeScript
Ваш код почти правильный, но TypeScript не может автоматически определить, что возвращение null корректно в определенных условиях. Основная проблема заключается в том, что система типов не может вывести, что null является допустимым возвращаемым значением, когда T включает null или undefined.
Ваш утилитарный тип NullabilityOf<T, U> определен правильно:
type NullabilityOf<T, U> = T extends null | undefined
? U | Extract<T, null | undefined>
: U;
Однако при использовании в методе from возникают сложности с выводом типов. TypeScript не понимает, что return null соответствует типу NullabilityOf<T, PhotoHelper>, когда T включает null.
Встроенные утилитарные типы TypeScript для работы с null/undefined
TypeScript предоставляет несколько встроенных утилитарных типов для работы с null и undefined:
NonNullable<T>- удаляетnullиundefinedиз типа:
type DefinitelyString = NonNullable<string | null | undefined>; // string
Nullable<T>- не существует в стандартной библиотеке, но легко создается:
type Nullable<T> = T | null;
UndefinedToOptional<T>- преобразует свойства с типомundefinedв необязательные:
type UndefinedToOptional<T> = T extends Record<string, undefined>
? { [K in keyof T]?: T[K] }
: T;
Для вашей задачи эти стандартные типы не подходят, так как вам нужно условное сохранение нулабельности на основе входного параметра.
Реализация пользовательского утилитарного типа для сохранения нулабельности
Вот правильная реализация вашего утилитарного типа и класса:
// Утилитарный тип для сохранения нулабельности
type NullabilityOf<T, U> = T extends null | undefined
? U | Extract<T, null | undefined>
: U;
interface Photo {
id: string;
}
export class PhotoHelper {
constructor(private image: Photo) { }
static from<T extends Photo | null | undefined>(
image: T
): NullabilityOf<T, PhotoHelper> {
if (image == null) {
return null as NullabilityOf<T, PhotoHelper>; // Корректный типовой привод
}
return new PhotoHelper(image);
}
get displayUri(): string {
return `uri_for_photo_${this.image.id}`;
}
}
Ключевое изменение - использование null as NullabilityOf<T, PhotoHelper> вместо null as any. Это явно указывает TypeScript, что null является допустимым значением для условного типа.
Альтернативные подходы без использования as any
Подход 1: Упрощение утилитарного типа
Вы можете упростить ваш утилитарный тип:
type NullabilityOf<T, U> = T extends null | undefined ? U | T : U;
Это более лаконично и сохраняет ту же функциональность.
Подход 2: Использование условного оператора возврата
Более элегантное решение - полностью отказаться от утилитарного типа и использовать условный оператор:
static from<T extends Photo | null | undefined>(
image: T
): T extends null | undefined ? PhotoHelper | null : PhotoHelper {
if (image == null) {
return null;
}
return new PhotoHelper(image);
}
Этот подход полностью избегает необходимости в утилитарном типе и предоставляет максимально точную типизацию.
Подход 3: Использование дженериков с ограничениями
Еще один вариант - использовать более строгие дженерики:
static from<T extends Photo>(
image: T
): PhotoHelper;
static from<T extends null | undefined>(
image: T
): null;
static from<T extends Photo | null | undefined>(
image: T
): PhotoHelper | null {
if (image == null) {
return null;
}
return new PhotoHelper(image);
}
Этот подход использует перегрузку методов для предоставления точной типизации.
Лучшие практики работы с нулабельностью в TypeScript
- Используйте строгую проверку на
null:
if (image === null) { // вместо image == null
// обрабатываем null
} else if (image === undefined) {
// обрабатываем undefined
} else {
// обрабатываем значение
}
- Рассмотрите использование оператора нулевого слияния (
??) для значений по умолчанию:
const result = image ?? defaultPhoto;
- Используйте оперцию необязательной цепочки (
?.) для безопасного доступа к свойствам:
console.log(image?.id); // безопасно для null и undefined
- Предпочитайте ненулевые assertion (
!) только тогда, когда вы уверены в значении:
const photo = image!; // вы утверждаете, что image не null
- Используйте утилитарные типы для сложных сценариев сохранения нулабельности.
В вашем случае наиболее предпочтительным является второй подход - использование условного оператора возврата в сигнатуре метода. Это не только избавляет от необходимости в as any, но и обеспечивает наиболее точную типизацию без лишних усложнений.
Источники
-
TypeScript Documentation — Официальная документация по утилитарным типам TypeScript: https://www.typescriptlang.org/docs/handbook/utility-types.html
-
GitHub TypeScript Issue — Обсуждение сохранения нулабельности в TypeScript: https://github.com/microsoft/TypeScript/issues/39522
Заключение
Для правильной реализации сохранения нулабельности в TypeScript без использования as any рекомендуется использовать один из предложенных подходов. Наиболее предпочтительным является условный оператор в сигнатуре метода, который обеспечивает точную типизацию без необходимости в дополнительных утилитарных типах или типовых утверждениях. Это решение полностью сохраняет безопасность типов и позволяет избежать распространенных ошибок при работе с нулабельными значениями в TypeScript.
TypeScript предоставляет встроенный утилитарный тип NonNullable<T>, который исключает null и undefined из типа T. Этот тип определяется как T extends null | undefined ? never : T. Он полезен для очистки union типов от нулевых значений. Например, type DefinitelyString = NonNullable<string | null | undefined> вернет тип string. Однако для сохранения нулабельности, как в вашем случае, потребуется создать собственный утилитарный тип. NonNullable влияет только на верхний уровень типа и не рекурсивно удаляет null/undefined из вложенных типов.

Команда TypeScript отклонила предложение о встроенном типе Nullable<T>, считая, что синтаксис T | null | undefined достаточно выразителен. В вашем коде проблема заключается в возврате null напрямую, тогда как тип ожидает U | T. Правильное решение - возвращать исходное значение image вместо null. Оптимальная реализация утилитарного типа: type NullabilityOf<T, U> = T extends null | undefined ? U | T : U;. При таком подходе TypeScript корректно выводит тип без необходимости использования as any. Также рекомендуется использовать строгую проверку === вместо == для лучшей типобезопасности.