generics - Hvordan sette en typeparameter til "nothing" i TypeScript?
I vår applikasjon har vi et data-«wrapper»-mønster der vi bruker en enkelt instans av en klasse for å representere noen data som kanskje ikke er lastet ennå. Noe sånt som dette:
abstract class BaseWrapper<TData> {
protected data?: TData;
loadedPromise: Promise<this>;
}
Deretter vil du implementere dette for et spesifikt datasett, slik:
interface PersonData {
name: string;
}
class PersonWrapper extends BaseWrapper<PersonData> {
get name() { return this.data && this.data.name }
}
Gitt en person:
const person: PersonWrapper = ...;
Typen til person.name er string | undefined siden this.data kan være undefined. Dette er bra, fordi det minner oss på at objektet kanskje ikke er lastet, og vi må håndtere det tilfellet når vi får tilgang til dets medlemmer.
Men i noen tilfeller vet vi at objektet er lastet, oftest når loadedPromise er oppfylt, slik:
(await person.loadedPromise).name
Typen på dette uttrykket er fortsatt string | undefined selv om vi vet at objektet er lastet. Dette er irriterende, og problemet jeg gjerne vil ha hjelp til å løse.
Min beste idé er å parameterisere BaseWrapper med en "tom" typeparameter. Noe slikt som dette:
abstract class BaseWrapper<TData, TEmpty = undefined> {
protected _data?: TData;
get data(): TData | TEmpty {
return this._data as (TData | TEmpty);
}
}
Da kunne vi gjort noe sånt som dette:
class PersonWrapper<TEmpty = undefined> extends BaseWrapper<PersonData, TEmpty> {
get name() { return this.data && this.data.name }
loadedPromise: Promise<PersonWrapper<nothing>>;
requireLoaded(): PersonWrapper<nothing> {
if (this.data) return this as PersonWrapper<nothing>
else throw new Error("Not loaded!")
}
}
Utstederen her er typen "nothing". Jeg vil kunne sende en type inn i parameteren slik at TData | TEmpty er lik TData. Hva bør denne typen være?
Forutsatt at det er mulig, ville typen til (await person.loadedPromise).name bare være string, noe som ville være fantastisk.
Noen ideer?
Svar #1
En mulig løsning er å bruke !-suffikset, som forsikrer kompilatoren om at en valgfri verdi er tilgjengelig:
const name: string = (await person.loadedPromise).name!
Dette fungerer i det helt åpenbare tilfellet hvor programmereren «vet» at verdien er lastet.
(hei, jeg riffet på ideen din om "TData | TEmpty er lik TData". Du vil ha en type som sikkert og transparent innkapsler mulig tomhet, uten at du trenger å håndtere det udefinerte tilfellet selv når du vet at verdien er definert...)
En annen tanke er denne: i stedet for at typen til name skal være string | undefined, hvorfor ikke la den være... string[]! Ha ha ha, hold ut med meg!
type Maybe<T> = T[] // I know, I know... this is weird
function of <T>(value: T): Maybe<T> {
return value === undefined ? [] : [value]
}
abstract class BaseWrapper<TData> {
protected data?: TData;
loadedPromise: Promise<this>;
}
interface PersonData {
name: string;
}
class PersonWrapper extends BaseWrapper<PersonData> {
get name() { return of(this.data && this.data.name) }
}
const person: PersonWrapper = ...;
// these typings will be inferred I'm just putting them here to
// emphasize the fact that these will both have the same type
const unloadedName: Maybe<string> = person.name
const loadedName: Maybe<string> = (await person.loadedPromise).name
Nå, hvis du vil bruke noen av disse navnene...
// reduce will either return just the name, or the default value
renderUI(unloadedName.reduce(name => name, 'defaultValue')
renderUI(loadedName.reduce(name => name, 'defaultValue')
Typen Maybe representerer en verdi som kan være udefinert, og den har ett trygt og konsistent grensesnitt for å få tilgang til den uansett tilstand. Standardverdien til en reduce er ikke en valgfri parameter, så det er ingen sjanse for å glemme den: du må alltid håndtere den udefinerte tilstanden, noe som er veldig gjennomsiktig! Hvis du vil gjøre litt mer behandling, kan du gjøre det med map.
unloadedName
.map(name => name.toUpperCase)
.map(name => leftPad(name, 3))
.map(name => split('')) // <-- this actually changes the type
.reduce(names => names.join('_'), 'W_A_T') // <-- now it's an array!
Dette betyr at du kan skrive funksjoner som,
function doStuffWithName(name: string) {
// TODO implement this
return name
}
og kall dem slik:
const name = person.name
.map(doStuffWithName)
.reduce(name => name, 'default')
renderUI(name)
og hjelpefunksjonene dine trenger ikke å vite at de håndterer en valgfri verdi. På grunn av måten map fungerer på, hvis verdien mangler, vil name være [] og map vil bruke den gitte funksjonen 0 ganger... altså på en trygg måte!
Hvis du føler deg ukomfortabel med å overbelaste array-typen på denne måten, kan du alltid implementere Maybe som en klasse: den trenger bare map og reduce. Du kan til og med implementere map og reduce på din wrapper-klasse (siden en Maybe bare er en wrapper for muligens udefinerte data).
Dette er ikke en "typescript-spesifikk" løsning med en "nothing type", men kanskje det vil hjelpe deg å finne løsningen din!
(MERK: Dette er mye som å eksponere den underliggende løftet, noe jeg også tror er noe du kanskje vil vurdere)
