Reshaping associations, generalization of GroupBy

One approach is to employ a helper function that unwraps singleton lists:

{delist[v_]} ^:= v

With this, the GroupBy expression is fairly succinct:

dataset // GroupBy[{#type&, #subtype& -> delist}]

(*
  <| "a" -> <| "I" -> <|"type" -> "a", "subtype" -> "I", "value" -> 1|>
             , "II" -> <|"type" -> "a", "subtype" -> "II", "value" -> 2|>
             |>
   , "b" -> <| "I" -> <|"type" -> "b", "subtype" -> "I", "value" -> 1|>
             , "II" -> <|"type" -> "b", "subtype" -> "II", "value" -> 2|>
             |>
   |>
*)

This generalizes to deeper nesting:

dataset // GroupBy[{#type&, #subtype&, #type&, #subtype& -> delist}]

(*
  <| "a" ->
      <| "I" -> <|"a" -> <|"I" -> <|"type" -> "a", "subtype" -> "I", "value" -> 1|>|>|>
       , "II" -> <|"a" -> <|"II" -> <|"type" -> "a", "subtype" -> "II", "value" -> 2|>|>|>
       |>
   , "b" ->
       <| "I" -> <|"b" -> <|"I" -> <|"type" -> "b", "subtype" -> "I", "value" -> 1|>|>|>
        , "II" -> <|"b" -> <|"II" -> <|"type" -> "b", "subtype" -> "II", "value" -> 2|>|>|>
        |>
   |>
*)

Instead of a nested association solution, would Query and Select be acceptable.

Query[Select[#type == "a" && #subtype == "I" &], "value"]@dataset

(* {1} *)

This form is more descriptive on what is happening and does not require reshaping of the list of associations.

If your data is such that there is only ever one item intersecting a particular "type" and "subtype" then tack on First.

First@Query[Select[#type == "a" && #subtype == "I" &], "value"]@
  dataset

(* 1 *)

Hope this helps.


Extension

You can extend this to a more general case in which you parametrise the filter by both key and value.

filterBy[filter_] := Function[Evaluate[And @@ ReleaseHold[Hold[Slot][First@#] == Last@# & /@ filter]]]

then with

target = {{"type", "a"}, {"subtype", "I"}};

Query[SelectFirst[filterBy[target]], "value"]@dataset

(* 1 *)

I have posted code doing a very similar thing here - the functions pushUp and pushUpNested. That code was more general, since there I provided a declarative interface to group by values or their parts. To do what you need, I'll redefine slightly (assuming you run that code):

ClearAll[pushUpNested];
pushUpNested[{}, elemF_: Identity] := elemF;
pushUpNested[specs : {_List ..}, elemF_: Identity ] := 
   Composition[
     Map[pushUpNested[Rest[specs], elemF]], 
     pushUp@First[specs]
   ];

Now we create a transform:

transform = pushUpNested[{{"type"}, {"subtype"}}, First]


(* 
   Map[Map[First]@*GroupBy[#1[[Sequence[Key["subtype"]]]] &]]@*
   GroupBy[#1[[Sequence[Key["type"]]]] &]
*)

which we can now apply to get the nested structure:

nested = transform@dataset

(*

   <|
     "a" -> <|
       "I" -> <|"type" -> "a", "subtype" -> "I", "value" -> 1|>, 
       "II" -> <|"type" -> "a", "subtype" -> "II", "value" -> 2|>
     |>, 
     "b" -> <|
       "I" -> <|"type" -> "b", "subtype" -> "I", "value" -> 1|>, 
       "II" -> <|"type" -> "b", "subtype" -> "II", "value" -> 2|>
     |>
   |>

*)

The advantage of using pushUpNested is that it makes it very easy and declarative to construct such transforms, and the transform is available for inspection as a stand-alone fully-prepared function.