Typescript - generický typový strážca pre funkciu isEmpty
Nemôžem správne implementovať generickú funkciu isEmpty(value) z hľadiska zužovania typovej obmedzenia poskytnutej hodnoty na jej prázdny protiklad.
Použitie:
function getCountryNameById(countries: LookupItem[] = [], countryId?: number): string | undefined {
if (isEmpty(countries) || !isNumber(countryId)) {
// within this branch I would like to get countries argument to be narrowed to empty array type.
// Same would apply for other function which can have argument type of object or string. Why ? -> to prevent someone to do some mad code hacks like accessing non existent value from empty array ( which would happen on runtime ofc ) on compile time
// $ExpectType []
console.log(countries)
return
}
// continue with code logic ...
// implementation ...
}
podobný prípad pri obmedzovaní objektu:
function doSomethingWithObject( data: { foo: string; bar: number } | object ){
if(isEmpty(data)){
// $ExpectType {}
data
// following should throw compile error, as data is empty object
data.foo.toUpercase()
return
}
// here we are sure that data is not empty on both runtime and compile time
}
implementácia ochrany typu isEmpty:
export const isEmpty = <T extends AllowedEmptyCheckTypes>(
value: T | AllowedEmptyCheckTypes
): value is Empty<T> => {
if (isBlank(value)) {
return true
}
if (isString(value) || isArray(value)) {
return value.length === 0
}
if (isObject(value)) {
return Object.keys(value).length === 0
}
throw new Error(
`checked value must be type of string | array | object. You provided ${typeof value}`
)
}
S definovanými typmi:
type EmptyArray = Array<never>
type Blank = null | undefined | void
/**
* // object collects {} and Array<any> so adding both {} and Array<any> is not needed
* @private
*/
export type AllowedEmptyCheckTypes = Blank | string | object
/**
* Empty mapped type that will cast any AllowedEmptyCheckTypes to empty equivalent
* @private
*/
export type Empty<T extends AllowedEmptyCheckTypes> = T extends string
? ''
: T extends any[]
? EmptyArray
: T extends object ? {} : T extends Blank ? T : never
Je to trochu zvláštne, pretože je to správne zúžené z hľadiska typu, ale nie v rámci vetvy if/else:
isEmpty pre hodnoty reťazca
isEmpty pre hodnoty poľa
isEmpty pre hodnoty objektu
kód je možné vidieť tu: https://github.com/Hotell/rex-tils/pull/13/files#diff-a3cdcb321a05315fcfc3309031eab1d8R177
Súvisiaca otázka: Typový ochranca pre prázdny objekt
Odpoveď č. 1
Jedným zo spôsobov, ako riešiť tento problém, je oddeliť kontroly nulových hodnôt (undefined, null) od kontrol prázdnych hodnôt ('', [], {}). Zvyčajne na to používam dve strážne funkcie typov — isDefined a isEmpty.
Prvý môže vyzerať takto. Všimnite si kontrolu typeof — vďaka nej to funguje aj s nedeclarovanými premennými.
function isDefined<T>(value: T | undefined | null): value is T {
return (typeof value !== 'undefined') && (value !== null);
}
Pre prázdne hodnoty možno použiť nasledujúci model.
namespace Empty {
export type String = '';
export type Object = Record<string, never>;
export type Array = never[];
}
type Empty =
| Empty.Array
| Empty.Object
| Empty.String;
function isEmpty<T extends string | any[] | object>(subject: T | Empty): subject is Bottom<T> {
switch (typeof subject) {
case 'object':
return (Object.keys(subject).length === 0);
case 'string':
return (subject === '');
default:
return false;
}
}
type Bottom<T> =
T extends string
? Empty.String
: T extends any[]
? Empty.Array
: T extends object
? Empty.Object
: never;
Spodné hodnoty sú správne odvodené.
declare const foo: 'hello' | Empty.String;
declare const bar: [number, number] | Empty.Array;
declare const baz: Window | Empty.Object;
if (isEmpty(foo) && isEmpty(bar) && isEmpty(baz)) {
console.log(foo, bar, baz);
}
Upraviť: pridané obmedzenia na T podľa návrhu.
Odpoveď č. 2
Takže po niekoľkých diskusiách na Twitteri a dlhom hľadaní na SO/Github som skončil s nasledujúcim riešením:
- predovšetkým kontrola null/undefined v rámci isEmpty nedáva veľký zmysel (hoci lodash.isEmpty to spracuje, podľa môjho názoru robí príliš veľa a nie veľmi explicitným spôsobom)
- pretože v podstate neexistuje žiadny rozdiel medzi
{}|object|any[], ochrana typu pomocou type guard by nikdy nefungovala očakávaným spôsobom - konečné riešenie akceptuje ako argumenty iba platné hodnoty na kontrolu -> js objekty a reťazce a ochrana vracia
never, takže zodpovedajúce hodnoty by mali typnever, pretože aj tak nedáva zmysel vykonávať akýkoľvek ďalší prihlásený kód v rámciif(isEmpty(value)){ ... }vyhlásenia, namiesto toho je lepšie ukončiť program alebo vyhodiť chybu
tu je konečná implementácia:
const isEmpty = <T extends string | object | any[]>(
value: T
): value is never => {
if (isString(value) || isArray(value)) {
return value.length === 0
}
if (isObject(value)) {
return Object.keys(value).length === 0
}
throw new Error(
`checked value must be type of string | array | object. You provided ${
// tslint:disable-next-line:strict-type-predicates
value === null ? 'null' : typeof value
}`
)
}
https://github.com/Hotell/rex-tils/pull/13/files#diff-68ff3b6b6a1354b7277dfc4b23d99901R50
