I have an object of data I need to pass to one of two places based on which union member the object currently contains. Both places need all of the data in the object so recreating the object with the narrowed type seems a little silly. Obviously it works though.
As an alternative, I tried making the object's interface generic over the union so the interface can represent the object in all three places, thinking the automatic type narrowing might apply to the type parameter of TestData
.
interface UserData {
kind: 'user',
user: string,
}
interface ServerData {
kind: 'server',
url: string,
}
type DataTypes = UserData | ServerData
interface TestData<D extends DataTypes> {
data: D,
id: string,
}
Now the top class can use TestData<DataTypes>
and the children can use TestData<UserData>
or TestData<ServerData>
. This works fine until you try and pass the object down to one of the children. The compiler will correctly narrow TestData
's data
property, but this doesn't narrow the type of the actual object, which still has a type of TestData<DataTypes>
. Here's an example.
function basicNarrow(test: TestData<DataTypes>) {
if (test.data.kind === 'user') {
// Correctly narrowed to `UserData`
test.data.user
// Error: Generic type not narrowed, still `TestData<DataTypes>
const typed: TestData<UserData> = test
} else {
// Correctly narrowed to `ServerData`
test.data.url
// Error: Generic type not narrowed, still `TestData<DataTypes>
const typed: TestData<ServerData> = test
}
}
At this point I could either use a type assertion or (again) create a new object to pass down the correct type, but after some digging I found this answer to a similar question which gives me
type NarrowKind<T, N> = T extends { kind: N } ? T : never;
function predicateNarrow(test: TestData<DataTypes>) {
const predicate = <K extends DataTypes['kind']>(narrow: TestData<DataTypes>, kind: K): narrow is TestData<NarrowKind<DataTypes, K>> => (
narrow.data.kind === kind
)
if (predicate(test, 'user')) {
// Correctly narrowed to `UserData`
test.data.user
// Success! Generic type narrowed to `TestData<UserData>
const typed: TestData<UserData> = test
} else {
// Error: Not narrowed
test.data.url
// Error: Generic type not narrowed, still `TestData<DataTypes>
const typed: TestData<ServerData> = test
}
}
This does what I was after inside the if
block, but the compiler won't narrow to the alternate case in the else
block without another explicit check the way it would if data
was just a local variable.
Here's an example of what I'd like the narrowed types to end up being ideally
function idealNarrow(test: TestData<DataTypes>) {
function isKind(/*???*/) { /*???*/ }
if (isKind(test, 'user')) {
const user: UserData = test.data
const typed: TestData<UserData> = test
} else {
const server: ServerData = test.data
const typed: TestData<ServerData> = test
}
}
Either of the solutions could be used without issue, but predicateNarrow(...)
is so close to what I was looking for, is there a way to combine these two behaviors somehow to narrow the whole generic TestData<D>
type automatically in the else block?