Typescript - Använda generics för att utöka ett gränssnitt med en egenskap av unionstyp
Jag har ett gränssnitt som detta
interface Test {
values: string[] | number[] | Date[]
// more properties
}
Jag skulle vilja skapa ett annat gränssnitt som gör typen specifik. Det jag försöker är:
interface TestWithType<T extends string | number | Date> extends Test {
values: T[];
}
Men detta misslyckas med ett fel som säger Typ 'string' kan inte tilldelas typen 'Date'.
Vad är den korrekta syntaxen för detta? Om egenskapen values inte var en array, skulle detta fungera bra
interface Test {
values: string | number | Date
}
interface TestWithType<T extends string | number | Date> extends Test {
values: T;
}
Svar #1
Som du har märkt får du inte string[] | number[] | Date[] genom att använda apply och skapa en array av element av typen string | number | Date. Och eftersom (string | number | Date)[] är bredare än string[] | number[] | Date[] får du ett kompileringsfel eftersom du inte kan vidga egenskaper hos subtyper.
@MattMcCutchens svar ger några sätt att hantera detta. Här är ett annat sätt:
Om du har en union som string | number | Date och vill programmatiskt omvandla den till en union av arrayer som string[] | number[] | Date[], kan du använda distributiva villkorstyper:
type DistributeArray<T> = T extends any ? T[] : never;
Då kan du definiera TestWithType i termer av DistributeArray:
// no error:
interface TestWithType<T extends string | number | Date> extends Test {
values: DistributeArray<T>;
}
Och kontrollera att det beter sig som du förväntar dig:
declare const testWithString: TestWithType<string>
testWithString.values; // string[]
declare const testWithDate: TestWithType<Date>
testWithDate.values; // Date[]
declare const testWithStringOrNumber: TestWithType<string | number>
testWithStringOrNumber.values; // string[] | number[]
Hoppas det hjälper. Lycka till!
REDIGERA :
Som en relaterad fråga, finns det något sätt att förbjuda att ett unionstyp passerar till generiken? (Som i att kräva att endast ange högst en av string, number eller date)
Ja, det är möjligt, men det kräver att man missbrukar typsystemet på ett sätt som gör mig obekväm. Jag skulle föreslå att inte göra det om du inte behöver. Här är det:
type DistributeArray<T> = T extends any ? T[] : never;
type NotAUnion<T> = [T] extends [infer U] ? U extends any ?
T extends U ? unknown : never : never : never
type ErrorMsg = "NO UNIONS ALLOWED, PAL"
interface TestWithType<T extends (
unknown extends NotAUnion<T> ? string | number | Date : ErrorMsg
)> extends Test {
values: DistributeArray<T>
}
declare const testWithString: TestWithType<string> // okay
declare const testWithDate: TestWithType<Date> // okay
declare const testWithStringOrNumber: TestWithType<string | number> // error:
// 'string | number' does not satisfy the constraint '"NO UNIONS ALLOWED, PAL"'.
Typen NotAUnion<T> utvärderas till unknown (en topp-typ) om T inte är en union... annars utvärderas den till never (en botten-typ). Sedan, i TestWithType är typen T begränsad till unknown extends NotAUnion<T> ? string | number | Date : ErrorMsg, vilket nästan bryter reglerna mot cirkulära referenser. Om du skickar en icke-union som T, blir det T extends string | number | Date som tidigare. Om du skickar en union, blir det T extends ErrorMsg, en string-literal typ. Eftersom en union aldrig kommer att utvidga en string-literal, kommer det att misslyckas... Och felmeddelandet du får kommer att involvera ErrorMsg, så det borde förhoppningsvis vara tillräckligt för att en utvecklare ska förstå vad som händer. Det fungerar, men det är väldigt, väldigt skakigt. Du bad dock om det.
Lycka till igen!
Svar #2
Två möjliga lösningar, med användning av uppslagstyper eller villkorstyper:
enum TestType { STRING, NUMBER, DATE }
interface TestTypeMapping {
[TestType.STRING]: string[];
[TestType.NUMBER]: number[];
[TestType.DATE]: Date[];
}
interface TestWithType1<T extends TestType> extends Test {
values: TestTypeMapping[T];
}
type TypeToArrayType<T> =
T extends string ? string[] :
T extends number ? number[] :
T extends Date ? Date[] :
never[];
interface TestWithType2<T extends string | number | Date> extends Test {
values: TypeToArrayType<T>;
}
