Can you Strict Generic types or give one parameter more than one type ?
Let's suppose that you could use a OR operator to combine protocols, what would you be able to do with something of type (Int | String)
?
Not everything you can do to an Int
can be done on (Int | String)
, because it might be a string underlyingly. Similarly, not everything you can do to an String
can be done on (Int | String)
, because it might be a Int
underlyingly.
Now you might say "Ah. I know that Int
and String
both have a description
property. I should be able to access description
on a variable of type (Int | String)
."
Well, in that case, you can just create such a protocol yourself and only have Int
and String
conform to it:
protocol IntOrString {
var description: String { get }
}
extension Int : IntOrString {}
extension String : IntOrString {}
(Note that description
is already defined in CustomStringConvertible
. For the sake of argument, imagine that does not exist.)
Inferred type in generic with multiple type parameters
You are close with foo2
. Here is what it's supposed to be to make it work.
declare function foo2<T, K = keyof T>(x: K)
let t2 = foo2<Bar>('t');
Instead of expecting a new generic type parameter, you can assign one to a type (in this case the type of T
).
It's also possible to directly assign the paramaters type to the keyof
the given generic type T
.
declare function foo4<T>(x: keyof T);
let t4 = foo4<Bar>('t');
Can we use generic to allow only specific types instead of any type T?
Make the constructor private:
private C(T param){
And then provide static factory methods to create instances of particular types:
public static <T extends A> C<T> create(T param) {
return new C<>(param);
}
public static <T extends B> C<T> create(T param) {
return new C<>(param);
}
This doesn't prevent you from using the type C<SomeOtherType>
; you just can't create an instance of it.
restrict generic type T to specific types
Short answer: Nope.
Typescript is structural, not nominal. That makes that if two types has the same interface, then they are the same type. That even includes class instances versus object literals.
We can test this with something like:
type Exact<A, B> = A extends B ? B extends A ? true : false : false
This type alias takes two parameters. If A extends B, and vice versa, then we know typescript considers them to be identical.
type IBase_Base = Exact<IBase, Base> // true
type Base_Derived = Exact<Base, Derived> // true
type Derived_ObjLiertal = Exact<Base, { prop1: string }> // true
So class Derived extends Base { }
is identical to Base
as far as typescript is concerned. in fact a class instance and an object literal are considered identical as long as the instance has the shape as the literal.
That said, if the derived class was different in anyway, then you could notice that with the help if this Exact
type and forbid it.
First you could make a type to enforce that a type exactly match, like:
type EnforceExact<Constraint, T> = Exact<Constraint, T> extends true ? T : never
Then make your function use this helper:
function doSomething<T extends IBase>(obj: EnforceExact<IBase, T>) {
// do something
}
Now if we make the Derived
class different somehow:
class DerivedWithAddition extends Base {
foo: number = 123
}
Then we get a type error when we try to pass this in:
doSomething(new DerivedWithAddition()) // not assignable to parameter of type 'never'.
Playground
You could also use the brand workaround as suggested in the answer that @kaya3 linked. However, unless there's a very good reason, you probably shouldn't. This structural equivalence is a pretty great feature of typescript. Your life will be easier if you just let someone pass in whatever they want, as long as it has the correct interface.
What is the best way to pass generic function that resolves to multiple types
You can't do this in F# with functions. Functions lose genericity when passed as values.
However, F# does have a mechanism for doing it anyway, albeit a bit awkwardly: interfaces. Interface methods can be generic, so you can use them to wrap your generic functions:
type Wrapper =
abstract member f<'a> : 'a -> 'a
let y (w: Wrapper) =
let a = w.f 1
let b = w.f 2L
(a, b)
let genericFn x = x
// Calling y:
y { new Wrapper with member __.f x = genericFn x }
The downside is, you can't go back to higher-order functions, lest you lose genericity. You have to have interfaces all the way down to the turtles. For example, you can't simplify the instance creation by abstracting it as a function:
let mkWrapper f =
// no can do: `f` will be constrained to a non-generic type at this point
{ new Wrapper with member __.f x = f x }
But you can provide some convenience on the other side. At least get rid of type annotations:
type Wrapper = abstract member f<'a> (x: 'a): 'a
let callF (w: Wrapper) x = w.f x
let y w =
let a = callF w 1
let b = callF w 2L
(a,b)
(NOTE: there may be minor syntactic mistakes in the above code, as I'm writing on my phone)
strictFunctionTypes restricts generic type
This is at it's core an issue of variance. So first a variance primer:
About variance
Given a generic type Foo<T>
, and two related types Animal
and Dog extends Animal
. There are four possible relationships between Foo<Animal>
and Foo<Dog>
:
- Covariance - The arrow of inheritance points in the same direction for
Foo<Animal>
andFoo<Dog>
as it does forAnimal
andDog
, soFoo<Dog>
is a sub type ofFoo<Animal>
, which also meansFoo<Dog>
is assignable toFoo<Animal>
type CoVariant<T> = () => T
declare var coAnimal: CoVariant<Animal>
declare var coDog: CoVariant<Dog>
coDog = coAnimal; // br>coAnimal = coDog; // ✅
- Contravariance - The arrow of inheritance points in the opposite direction for
Foo<Animal>
andFoo<Dog>
as it does forAnimal
andDog
, soFoo<Animal>
is a actually sub type ofFoo<Dog>
, which also meansFoo<Animal>
is assignable toFoo<Dog>
type ContraVariant<T> = (p: T) => void
declare var contraAnimal: ContraVariant<Animal>
declare var contraDog: ContraVariant<Dog>
contraDog = contraAnimal; // ✅
contraAnimal = contraDog; // br>
- Invariance - Although
Dog
andAnimal
are relatedFoo<Animal>
andFoo<Dog>
have no relationship whatsoever between them, so neither is assignable to the other.
type InVariant<T> = (p: T) => T
declare var inAnimal: InVariant<Animal>
declare var inDog: InVariant<Dog>
inDog = inAnimal; // br>inAnimal = inDog; // br>
- Bivariance - If
Dog
andAnimal
are related, bothFoo<Animal>
is a subtype ofFoo<Dog>
andFoo<Animal>
is a subtype ofFoo<Dog>
meaning either type is assignable to the other. In a stricter type system, this would be a pathological case, whereT
might not actually be used, but in typescript, methods parameter positions are considered bi-variant.
class BiVariant<T> { m(p: T): void {} }
declare var biAnimal: BiVariant<Animal>
declare var biDog: BiVariant<Dog>
biDog = biAnimal; // ✅
biAnimal = biDog; // ✅
All Examples - Playground Link
So the question is how does the usage of T
impact variance? In typescript the position of a type parameter determines variance, some examples :
- Co-varaint -
T
is used in as a field or as the return type of a function - Contra-varaint -
T
is used as the parameter of a function signature understrictFunctionTypes
- Invariant -
T
is used in both a covariant and contravariant position - Bi-variant -
T
is used as the parameter of a method definition understrictFunctionTypes
, or as the parameter type of either method or function ifstrictFunctionTypes
are off.
The reasoning for the different behavior of method and function parameters in strictFunctionTypes
is explained here:
The stricter checking applies to all function types, except those originating in method or constructor declarations. Methods are excluded specifically to ensure generic classes and interfaces (such as Array) continue to mostly relate covariantly. The impact of strictly checking methods would be a much bigger breaking change as a large number of generic types would become invariant (even so, we may continue to explore this stricter mode).
Back to the question
So lets see how, the usages of T
impact the variance of Foo
.
barCallback!: (val: T) => void;
- used as a parameter in member that is a function -> contra variant positionbaz(callback: ((val: T) => void)): void
- used as a parameter in the callback parameter of another function. This is a bit tricky, spoiler alert, this will turn out to be covariant. Lets consider this simplified example:
type FunctionWithCallback<T> = (cb: (a: T) => void) => void
// FunctionWithCallback<Dog> can be assigned to FunctionWithCallback<Animal>
let withDogCb: FunctionWithCallback<Dog> = cb=> cb(new Dog());
let aliasDogCbAsAnimalCb: FunctionWithCallback<Animal> = withDogCb; // ✅
aliasDogCbAsAnimalCb(a => a.animal) // the cb here is getting a dog at runtime, which is fine as it will only access animal members
let withAnimalCb: FunctionWithCallback<Animal> = cb => cb(new Animal());
// FunctionWithCallback<Animal> can NOT be assigned to FunctionWithCallback<Dog>
let aliasAnimalCbAsDogCb: FunctionWithCallback<Dog> = withAnimalCb; // br>aliasAnimalCbAsDogCb(d => d.dog) // the cb here is getting an animal at runtime, which is bad, since it is using `Dog` members
Playground Link
In the first example, the callback we pass to aliasDogCbAsAnimalCb
expects to receive an Animal
, so it only uses Animal
members. The implementation withDogCb
will create a Dog
and pass it to the callback, but this is fine. The callback will work as expected using just the base class properties it expects are there.
In the second example, the callback we pass to aliasAnimalCbAsDogCb
expects to receive a Dog
, so it uses Dog
members. But the implementation withAnimalCb
will pass into the callback an instance of an animal. This can leas to runtime errors as the callback ends up using members that are not there.
So given it is only safe to assign FunctionWithCallback<Dog>
to FunctionWithCallback<Animal>
, we arrive at the conclusion that such a usage of T
determines covariance.
Conclusion
So we have T
used in both a covariant and a contravariant position in Foo
, this means that Foo
is invariant in T
. This means that Foo<any[] | { [s: string]: any }>
and Foo<any[]>
are actually unrelated types as far as the type system is concerned. And while overloads are looser in their checks, they do expect the return type of the overload and the implementation to be related (Either the implementation return or the overloads return must be a subtype of the other, ex)
Why some changes make it work:
- Turning off
strictFunctionTypes
will make thebarCallback
site forT
bivariant, soFoo
will be covariant - Converting
barCallback
to a method, makes the site forT
bivariant soFoo
will be covariant - Removing
barCallback
will remove the contravariant usage and soFoo
will be covariant - Removing
baz
will remove the covariant usage ofT
makingFoo
contravariant inT
.
Workarounds
You can keep strictFunctionTypes
on and carve out an exception just for this one callback to keep it bivariant, by using a bivariant hack (explained here for a more narrow use case, but the same principle applies):
type BivariantCallback<C extends (... a: any[]) => any> = { bivarianceHack(...val: Parameters<C>): ReturnType<C> }["bivarianceHack"];
class Foo<T> {
static manyFoo(): Foo<any[] | { [s: string]: any }>;
static manyFoo(): Foo<any[]> {
return ['stub'] as any;
}
barCallback!: BivariantCallback<(val: T) => void>;
constructor() {
// get synchronously from elsewhere
(callback => {
this.barCallback = callback;
})((v: any) => {});
}
baz(callback: ((val: T) => void)): void {}
}
Playground Link
Typescript generic type parameters: T vs T extends {}
Note: I'll assume you're using a version of TypeScript 3.5 or later; in TypeScript 3.5 a change was made so that generic type parameters are implicitly constrained by unknown
instead of the empty object type {}
, and some minor details about the difference between funcA()
and funcB()
changed. I don't want to make a long post even longer by talking about how things used to be in TS3.4 and below.
If you don't explicitly constrain a generic type parameter via extends XXX
, then it will implicitly be constrained by unknown
, the "top type" to which all types are assignable. So in practice that means the T
in funcA<T>()
could be absolutely any type you want.
On the other hand, the empty object type {}
, is a type to which nearly all types are assignable, except for null
and undefined
, when you have enabled the --strictNullChecks
compiler option (which you should). Even primitive types like string
and number
are assignable to {}
.
So compare:
function funcA<T>() { }
funcA<undefined>(); // okay
funcA<null>(); // okay
funcA<string>(); // okay
funcA<{ a: string }>(); // okay
with
function funcB<T extends {}>() { }
funcB<undefined>(); // error
funcB<null>(); // error
funcB<string>(); // okay
funcB<{ a: string }>(); // okay
The only difference is that T extends {}
forbids null
and undefined
.
It might be a little confusing that {}
, a so-called "object" type, would accept primitives like string
and number
. It helps to think of such curly-brace-surrounded types like {}
and {a: string}
as well as all interface
types not necessarily as "true" object types, but as types of values where you can index into them as if they were objects without getting runtime errors. Primitives except for null
and undefined
are "object-like" in that you can treat them as if they were wrapped with their object equivalents:
const s: string = "";
s.toUpperCase(); // okay
And therefore even primitives like string
are assignable to curly-brace-surrounded types as long as the members of those types match:
const x: { length: number } = s; // okay
If you really need to express a type that only accepts "true", i.e., non-primitive objects, you can use the object
:
const y: object & { length: number } = s; // error
const z: object & { length: number } = { length: 10 }; // okay
But I (seriously) digress.
Okay, hope that helps; good luck!
Playground link to code
Related Topics
How to Initialize UIbezierpath to Draw a Circle in Swift
How to Byte Reverse Nsdata Output in Swift The Littleendian Way
Enum Initialized with a Non-Existent Rawvalue Does Not Fail and Return Nil
How to Solve This Error: Thread 1: Exc_Resource Resource_Type_Memory (Limit=650 Mb, Unused=0X0)
Weak Reference to Closure in Swift
How to Draw a Line Between Two Points Over an Image in Swift 3
Shorthand for Wrapping a Swift Variable in an Optional
Why Is Inceptionv3 Machine Learning Model Not Recognized on My Project
Xcodebuild Commands Failed to Generate Ipa
Uisearchcontroller Searchbar Misaligns While Active During Rotation
How to Handle Error with Realm During Writing
Custom Vibration in iOS 8 - Swift
Enum of String Type Vs Struct with Static Constant
iOS Swift, Aws Cognito User Pool, Unable to Refresh Access Token
How to Print Http Request to Console
How to Find The Nth Root of a Value
Loading Image from Assets to Nsimage Keep Getting Error, Expecting Nsimage.Name