Skip to content

Commit

Permalink
Fixed type narrowing.
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanburke committed Dec 21, 2024
1 parent 955ff1a commit 825f594
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 4 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "functype",
"version": "0.8.45",
"version": "0.8.46",
"description": "A smallish functional library for TypeScript",
"author": "jordan.burke@gmail.com",
"license": "MIT",
Expand Down
5 changes: 3 additions & 2 deletions src/list/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type List<A> = {
filter<S extends A>(predicate: (a: A) => a is S): List<S>
filter(predicate: (a: A) => unknown): List<A>
filterNot: (p: (a: A) => boolean) => List<A>
filterType: <T extends A>(tag: ExtractTag<T>) => List<T>
filterType: <T extends A & Typeable<string, unknown>>(tag: string) => List<T>
find: <T extends A = A>(predicate: (a: A) => boolean, tag?: ExtractTag<T>) => Option<T>
readonly head: A
readonly headOption: Option<A>
Expand Down Expand Up @@ -76,7 +76,8 @@ const createList = <A>(values?: Iterable<A>): List<A> => {

filterNot: (p: (a: A) => boolean) => createList(array.filter((x) => !p(x))),

filterType: <T extends A>(tag: ExtractTag<T>) => createList(array.filter((x): x is T => isTypeable(x, tag))),
filterType: <T extends A & Typeable<string, unknown>>(tag: string) =>
createList(array.filter((x): x is T => isTypeable(x, tag))),

find: <T extends A = A>(predicate: (a: A) => boolean, tag?: ExtractTag<T>) => {
const result = array.find((x) => predicate(x) && (tag ? isTypeable(x, tag) : true))
Expand Down
2 changes: 1 addition & 1 deletion src/typeable/Typeable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function Typeable<Tag extends string, T>(tag: Tag, data: T): Typeable<Tag
}

// Type guard with automatic type inference using the full type
export function isTypeable<T>(value: unknown, tag?: ExtractTag<T>): value is T {
export function isTypeable<T>(value: unknown, tag: string): value is T {
if (!value || typeof value !== "object" || !("_tag" in value)) {
return false
}
Expand Down
103 changes: 103 additions & 0 deletions test/list/list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,107 @@ describe("List", () => {
})
})
})

describe("List Type Filtering", () => {
// First, let's define base interfaces for our data
type BaseNodeData = {
calculated: boolean | null
created: string | null
dataType: string
deleted: string | null
entityId: string
flowGridId: string
id: string
parentId: string | null
projectId: string
tenantId: string
updated: string | null
x: number
y: number
}

type BaseSegmentData = {
created: string | null
deleted: string | null
description: string | null
duration: number
externalId: string | null
fileName: string | null
id: string
imageUrl: string
parentDuration: number
parentId: string
projectId: string
segmentDuration: number[]
tags: string[]
tenantId: string
title: string
updated: string | null
url: string
}

// Then create our Typeable types
type FlowNodeData = Typeable<"FlowNode", BaseNodeData>
type SegmentData = Typeable<"SegmentData", BaseSegmentData>

// Create test data that exactly matches our types
const testData: (FlowNodeData | SegmentData)[] = [
Typeable<"FlowNode", BaseNodeData>("FlowNode", {
calculated: null,
created: null,
dataType: "node",
deleted: null,
entityId: "entity1",
flowGridId: "grid1",
id: "1",
parentId: null,
projectId: "proj1",
tenantId: "tenant1",
updated: null,
x: 0,
y: 0,
}),
Typeable<"SegmentData", BaseSegmentData>("SegmentData", {
created: null,
deleted: null,
description: null,
duration: 120,
externalId: null,
fileName: null,
id: "2",
imageUrl: "image.jpg",
parentDuration: 120,
parentId: "parent1",
projectId: "proj1",
segmentDuration: [60, 60],
tags: ["tag1"],
tenantId: "tenant1",
title: "Segment 1",
updated: null,
url: "video.mp4",
}),
]

const mixedData = List(testData)

it("should filter FlowNodes", () => {
const nodes = mixedData.filterType<FlowNodeData>("FlowNode")

expect(nodes.length).toBe(1)
nodes.forEach((node) => {
expect(node._tag).toBe("FlowNode")
expect(node.dataType).toBe("node")
})
})

it("should filter SegmentData", () => {
const segments = mixedData.filterType<SegmentData>("SegmentData")

expect(segments.length).toBe(1)
segments.forEach((segment) => {
expect(segment._tag).toBe("SegmentData")
expect(segment.duration).toBe(120)
})
})
})
})

0 comments on commit 825f594

Please sign in to comment.