admin管理员组

文章数量:1122832

Given a type Type who takes a record with a tuple including a union of strings and one string of that union (think of it as options and a default), and takes an union of string build such as each string match this pattern ${key of first type}:${one of the value in first type union}

type BuildPart<T extends Record<string, [string, string]>> = '' | {
    [K in keyof T]: K extends string ? `${K & string}:${T[K][number]}` : never
}[keyof T]

type Type<All extends Record<string, [string, string]>, Part extends BuildPart<All> = ''> = {
    [K in keyof All]: K extends string ? Part extends `${K}:${infer V}` ? V : All[K][1] : never
}[keyof All]

What i want to do is to retrieve values picked, ie either the value set as the original default or if one was provided by Part then pick that one.

type test0 = Type<{a: ['b' | 'c', 'c'], b: ['d' | 'e', 'd']}>                   // 'c' | 'd'
type test1 = Type<{a: ['b' | 'c', 'c'], b: ['d' | 'e', 'd']}, 'a:b'>            // 'b' | 'd
type test2 = Type<{a: ['b' | 'c', 'c'], b: ['d' | 'e', 'd']}, 'a:b' | 'b:e'>    // 'b' | 'c' | 'd' | 'e' but expected 'b' | 'e'

It works as expected with Part empty or holding a single value, but as soon as Part is an union then all possible values are rendered

playground

Given a type Type who takes a record with a tuple including a union of strings and one string of that union (think of it as options and a default), and takes an union of string build such as each string match this pattern ${key of first type}:${one of the value in first type union}

type BuildPart<T extends Record<string, [string, string]>> = '' | {
    [K in keyof T]: K extends string ? `${K & string}:${T[K][number]}` : never
}[keyof T]

type Type<All extends Record<string, [string, string]>, Part extends BuildPart<All> = ''> = {
    [K in keyof All]: K extends string ? Part extends `${K}:${infer V}` ? V : All[K][1] : never
}[keyof All]

What i want to do is to retrieve values picked, ie either the value set as the original default or if one was provided by Part then pick that one.

type test0 = Type<{a: ['b' | 'c', 'c'], b: ['d' | 'e', 'd']}>                   // 'c' | 'd'
type test1 = Type<{a: ['b' | 'c', 'c'], b: ['d' | 'e', 'd']}, 'a:b'>            // 'b' | 'd
type test2 = Type<{a: ['b' | 'c', 'c'], b: ['d' | 'e', 'd']}, 'a:b' | 'b:e'>    // 'b' | 'c' | 'd' | 'e' but expected 'b' | 'e'

It works as expected with Part empty or holding a single value, but as soon as Part is an union then all possible values are rendered

playground

Share Improve this question asked Nov 22, 2024 at 15:47 zedryaszedryas 97411 silver badges19 bronze badges 3
  • Are you asking us for an implementation of Type? Or to debug yours? – jcalz Commented Nov 22, 2024 at 17:06
  • 1 You cannot use a distributive conditional type on Part inside Type, or at least you can't have the false branch give you All[K][1]. Instead you should probably factor out the "find the suffix in a union" to a new utility type, such as shown in this playground link. Does that fully address the question? If so I'll write an answer; if not, what's missing? – jcalz Commented Nov 22, 2024 at 17:16
  • @jcalz your solution works fine - and if in your answer you could explain in it well why my type did not work it would be great - as i often struggle a lot when working wirh union – zedryas Commented Nov 22, 2024 at 19:44
Add a comment  | 

1 Answer 1

Reset to default 1

The problem is that

Part extends `${K}:${infer V}` ? V : All[K][1]

is a distributive conditional type and acts on all union members of Part separately. If any union member of Part fails to match K, then you'll get the default for K in the response (All[K][1]). Like, using your example above, when K is "a" and Part is "a:b" | "b:e", then first it evaluates "a:b" extends `a:${infer V}` ? V : "c" which takes the true branch and gives you "b", and then it evaluates "b:e" extends `a:${infer V}` ? V : "c" which takes the false branch and gives you "c". So you get "b" | "c", which you don't want.


We cannot have that All[K][1] in the false branch. Instead it needs to be never, so that the whole thing evaluates to just "b". Only in the case that the entire conditional type is never do we select "c". I'd rather break this out into its own utility type like

type GetSuffix<K extends string, P extends string> =
    P extends `${K}:${infer V}` ? V : never;

So for each K we want GetSuffix<K, P> for the entire union P, unless that's never, in which case we want All[K][1]. The "use this unless it's never, in which case use this other thing" can also be broken out to a utility type

type OrDefault<T, D> = [T] extends [never] ? D : T;

(and we really don't want that to be distributive). That makes Type look like

type Type<O extends Record<string, [string, string]>, P extends BuildPart<O> = never> = {
    [K in string & keyof O]: OrDefault<GetSuffix<K, P>, O[K][1]>
}[string & keyof O]

which is very similar to yours (although I've moved where we check for K being string around). Let's try it:

type test0 = Type<{ a: ['b' | 'c', 'c'], b: ['d' | 'e', 'd'] }>                   
//       ^? type test0 = 'c' | 'd'
type test1 = Type<{ a: ['b' | 'c', 'c'], b: ['d' | 'e', 'd'] }, 'a:b'>            
//       ^? type test1 = 'b' | 'd'
type test2 = Type<{ a: ['b' | 'c', 'c'], b: ['d' | 'e', 'd'] }, 'a:b' | 'b:e'>    
//       ^? type test2 = 'b' | 'e'

Looks good.

Playground link to code

本文标签: typescriptUnion breaks extraction of values in an record of stringsStack Overflow