1

I need to dynamically intersect the string types as shown below.

export class Foo<K extends string> {
    addKey<L extends string>(key: L): Foo<K | L> {
        return this;
    }
    getKey(key: K) {

    }
}

const x = new Foo().addKey('a').addKey('b');

// need to constraint this to 'a' | 'b'
x.getKey('')

http://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=13&pc=13#code/KYDwDg9gTgLgBAYwDYEMDOa4DEIQDwDScoMwAdgCaZoxQCWZA5gHxwDeAUHN3ChRQWABPPABliIUpWq0GLABQBrYQC44ogJRqc+IgB91rTjxNwowGAFcoZODAAWdNAG4uPAL5vujC4KFLVOAINdg4vOE9PDgQIMho4EDgAXjgyYAB3bFx5DQA6PgFheQByFGK8gr8SgCNy1w4AegbU4GAKOwhEWJooFAZ4BycOuFLiuANi2o4QXJ8YKuLyoA

3
  • OK... what is the question? Commented Dec 9, 2019 at 17:11
  • 1
    Please note that you are describing a union (|) and not an intersection (&). Commented Dec 9, 2019 at 19:20
  • @jcalz you are right. i checked it before and i thought it is intersection. now i see you are right Commented Dec 9, 2019 at 21:15

2 Answers 2

2

Presumably your issue is that new Foo() produces an instance of Foo<string>, and string absorbs any string literal types when joined via a union. That is, string | "a" | "b" is just string, which completely forgets all about "a" and "b".

All that needs to be done here is to pick a default value for K such that it represents the absence of any strings. Some type which extends string but which has no values, so that when you join "a" to it in a union, you get "a" out. Luckily that type exists, and it's called never. As a bottom type, never extends every type including string, and is absorbed by every other type when you join to it with a union. So never | "a" | "b" will be "a" | "b".

Here goes:

export class Foo<K extends string = never> { // K has a default now
    addKey<L extends string>(key: L): Foo<K | L> {
        return this;
    }
    getKey(key: K) {

    }
}

And let's test it:

const x = new Foo().addKey('a').addKey('b');
x.getKey('') // error!
// ----> ~~
// Argument of type '""' is not assignable to parameter of type '"a" | "b"'.

Looks good to me. Hope that helps; good luck!

Link to code

Sign up to request clarification or add additional context in comments.

4 Comments

and just out of curiosity, i use const x = new Foo() not to define the static type, but how i would do it if i would need to define the type like const x:Foo = new Foo()? If i use x: Foo the inferring stops working.
This is a separate question, right? Both const x = new Foo() and const x: Foo = new Foo() should do the same thing. But const x = new Foo().addKey('a') and const x: Foo = new Foo().addKey('a') are different, because you're chaining. Just like const x = ""; const y: string="" is different from const x = "".length; const y: string = "".length. The latter is incorrect because the whole expression is of a different type than just the first part. Not sure what your intent is, and a comment isn't a great place to explore much.
yes i understand it is different in each step of chain, but question is, if it is possible to define the type of x as Foo, because it seems that only way it works is to not define any type (this is the only case when it is inferred)
If you define it as Foo you are losing information, like saying const y: unknown = "" and then you're unable to access y.length. You can define it as const x: Foo<"a" | "b"> = new Foo().addKey('a').addKey('b'); which is cumbersome, so if I were you I'd just let the compiler infer the type. Not sure why you think you "need to define" it as Foo without a minimal reproducible example (what, specifically, doesn't work if you let the compiler infer the type), in a different question. I think I have to be done with this comment conversation now. Good luck to you!
1

There are a few ways to crack this egg

In general I would define a type alias for the subset of strings you want to serve as the union.

export class Foo<K extends string> {
    // note L extending K here, you may want to do it the other way
    addKey<L extends K>(key: L): Foo<K | L> {
        return this;
    }
    getKey(key: K) {

    }
}

type StringAlias = 'a' | 'b'
// Now all are constrained
const x = new Foo<StringAlias>().addKey('b').addKey('a');
x.getKey('a')

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.